Merge pull request #5 from gmemstr/refactoring

Huge refactoring, simplification and expansion.
This commit is contained in:
Gabriel Simmer 2020-04-24 09:25:51 +01:00 committed by GitHub
commit 8127aaa6a8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1586 additions and 24914 deletions

41
.circleci/config.yml Normal file
View file

@ -0,0 +1,41 @@
version: 2.1
orbs:
upx: circleci/upx@1.0.1
jobs:
build:
docker:
- image: cimg/go:1.14
steps:
- checkout
- restore_cache:
keys:
- go-mod-{{ checksum "go.sum" }}-v2
- go-mod-{{ checksum "go.sum" }}
- go-mod
- upx/install
- run:
command: make dist
- store_artifacts:
path: build
- save_cache:
key: go-mod-{{ checksum "go.sum" }}-v2
paths:
- /home/circleci/go/pkg/mod
test:
docker:
- image: cimg/go:1.14
steps:
- checkout
- restore_cache:
keys:
- go-mod-{{ checksum "go.sum" }}-v2
- go-mod-{{ checksum "go.sum" }}
- go-mod
- run:
command: make test
workflows:
version: 2
build-and-test:
jobs:
- build
- test

7
.gitignore vendored
View file

@ -14,12 +14,13 @@
# IntelliJ
.idea/
# Config file
config.json
# Config files
providers.yml
auth.yml
# Binary
nas
build/
*.db
.lock
assets/web/*

35
Makefile Normal file
View file

@ -0,0 +1,35 @@
.DEFAULT_GOAL := build
SLIPROAD_VERSION := 2.0.0
# Workaround for CircleCI Docker image and mkdir.
SHELL := /bin/bash
make_build_dir:
mkdir -p build/{bin,assets,tars}
build: make_build_dir
go build -o build/bin/sliproad
pi: make_build_dir
env GOOS=linux GOARCH=arm GOARM=5 go build -o build/bin/sliproad-arm
small: make_build_dir
go build -o build/bin/sliproad -ldflags="-s -w"
upx --brute build/bin/sliproad -9 --no-progress
small_pi: make_build_dir
env GOOS=linux GOARCH=arm GOARM=5 go build -o build/bin/sliproad-arm -ldflags="-s -w"
upx --brute build/bin/sliproad-arm -9 --no-progress
run:
go run webserver.go
test:
go test ./... -cover
dist: clean make_build_dir small small_pi
cp -r assets/* build/assets
tar -czf build/tars/sliproad-$(SLIPROAD_VERSION)-arm.tar.gz build/assets build/bin/sliproad-arm README.md LICENSE
tar -czf build/tars/sliproad-$(SLIPROAD_VERSION)-x86.tar.gz build/assets build/bin/sliproad README.md LICENSE
clean:
rm -rf build

View file

@ -1,30 +1,76 @@
# nas
small go nas platform for my raspberry pi
# sliproad
merging filesystems together
## usage
## about
```
cp assets/config/config.example.json assets/config/config.json
# edit config file with your hot/cold storage locations
nano assets/config/config.json
# run
go run webserver.go
# or build and run
go build; ./nas
this project aims to be an easy-to-user web API that allows the management of cloud storage, whether it be on
the host machine or part of a remote api. this is intended mostly for my own use, but i am documenting it in a way that
i hope allows others to pick it up and improve on it down the line.
if something is unclear, feel free to open an issue :)
## configuration
unlike the initial version of this project, the current build uses _providers_ to determine how to handle various
functions related to files. currently, two are implemented, `disk` and `backblaze`, since they are the primary providers
i use myself. the providers you would like to use can be added to `providers.yml` alongside the binary.
for example, here is a sample configuration implementing both of them:
```yaml
disk:
provider: disk
path: /tmp/nas
backblaze:
provider: backblaze
config:
appKeyId: APP_KEY_ID
appId: APP_ID
bucket: BUCKET_ID
```
you can also optionally use the `build-pi.sh` to build it for a raspberry pi (tested with raspberry pi 3 model b+)
(read more here: [#providers](#providers))
then navigate to `localhost:3000`
## running
## api
after adding the providers you would like to use, the application can be run simply with `./nas`. it will attach to port
`:3000`.
initially the heavy lifting was done by the server, but the need for a better frontend was clear.
## building
full documentation coming soon once actual functionality has been nailed down.
this project uses go modules and a makefile, so building should be relatively straightforward.
## credits
- `make` will build the project for your system's architecture.
- `make run` will run the project with `go run`
- `make pi` will build the project with the `GOOS=linux GOARCH=arm GOARM=5 go` flags set for raspberry pis.
## providers
svg icons via https://iconsvg.xyz
"providers" provide a handful of functions to interact nicely with a filesystem, whether it be on local disk or on a
remote server via an api. best-effort is done to keep these performant, up to date and minimal.
raspberry pi svg via https://www.vectorlogo.zone/logos/raspberrypi/index.html
there are a few built-in providers, and more can be added by opening a pull request.
|name|service|configuration example|
|----|-------|---------------------|
|disk|local filesystem|assets/config_examples/disk.yml|
|backblaze|backblaze b2|assets/config_examples/backblaze.yml|
you can find a full configuration file under `assets/config_examples/providers.yml`
### custom provider
custom file providers can be implemented by adding a new go file to the `files` module. it should
implement the `FileProviderInterface` interface.
## authentication
basic authentication support utilizing [keycloak](https://keycloak.org/) has been implemented, but work
is being done to bring this more inline with the storage provider implementation. see `assets/config_examples/auth.yml`
for an example configuration - having this file alongside the binary will activate authentication on all
`/api/files` endpoints. note that this implementation is a work in progress, and a seperate branch will
contain further improvements.
## icons
SVG Icons provided by Paweł Kuna: https://github.com/tabler/tabler-icons (see assets/web/icons/README.md)

View file

@ -1,4 +0,0 @@
{
"ColdStorage": "",
"HotStorage": ""
}

View file

View file

@ -0,0 +1,3 @@
provider_url: "http://localhost:8080"
realm: "nas"
redirect_base_url: "http://localhost:3000"

View file

@ -0,0 +1,9 @@
# The Backblaze provider requires an application key, application key ID, and bucket ID to use.
# You can find steps for generating there here: https://www.backblaze.com/b2/docs/application_keys.html
# Keys should have at least the listBuckets, readFiles, writeFiles and shareFiles permissions for a bucket.
backblaze:
provider: backblaze
config: # Provider-specific files.
applicationKeyId: aaaaaaaaaaaa
applicationKey: aaaaaaaaaaaa
bucket: aaaaaaaaaaaa

View file

@ -0,0 +1,5 @@
# The disk provider is the most basic of providers, requiring only a path on disk to write and retrieve files to and
# from.
disk:
provider: disk
path: /tmp/nas # This is only used for the `disk` provider right now, and indicates where to manage files.

View file

@ -0,0 +1,20 @@
# A "provider" is a service that provides access to a filesystem.
#
# A full configuration for every provider implemented in the application.
# You can find full breakdowns for each provider's configuration in it's respective file under
# `assets/config_examples/`.
#
# Schema is as follows:
# Provider Name: string - used to identify which filesystem to access.
# provider: string - should be one of the built-in providers.
# path: string - optional, just used for `disk` right now.
# config: mapping - used for provider-specific configuration values, such as authentication.
disk:
provider: disk
path: /tmp/nas
backblaze:
provider: backblaze
config:
applicationKeyId: aaaaaaaaaaaa
applicationKey: aaaaaaaaaaaa
bucket: aaaaaaaaaaaa

203
assets/web/css/styles.css Normal file
View file

@ -0,0 +1,203 @@
:root {
/* https://coolors.co/fe4a49-fed766-009fb7-e6e6ea-f4f4f8 */
--orange: rgba(254, 74, 73, 1);
--yellow: rgba(254, 215, 102, 1);
--blue: rgba(0, 159, 183, 1);
--platinum: rgba(230, 230, 234, 1);
--white: rgba(244, 244, 248, 1);
--desktop-width: 1170px;
}
body, h1 a {
font-family: sans-serif;
color: var(--orange);
background-color: var(--white);
}
.grid-lg {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 2fr);
grid-column-gap: 5px;
grid-row-gap: 5px;
}
.grid-lg a {
display: flex;
padding: 10vh;
justify-content: center;
font-size: 32px;
font-weight: bold;
text-decoration: none;
transition: background-color 0.5s;
background-color: var(--blue);
}
.list {
width: 40%;
margin: 0 auto;
display: flex;
flex-direction: column;
}
.list a {
padding: 1vh;
font-size: 24px;
font-weight: bold;
text-decoration: none;
text-align: center;
transition: background-color 0.5s;
-ms-word-wrap: anywhere;
word-wrap: anywhere;
margin: 5px 0;
position: relative;
border-radius: 5px 0 0 5px;
display: inline-block;
width: 93%;
}
.list a img {
left: 1vw;
top: 1vh;
position: absolute;
}
.list a.file {
background-color: var(--orange);
}
.list a.directory {
background-color: var(--blue);
}
.grid-lg a:visited, .grid-lg a,
.list a:visited, .list a {
color: var(--white);
}
.grid-lg a:hover,
.list a.directory:hover {
color: var(--blue);
background-color: var(--platinum);
transition: background-color 0.5s, color 0.5s;
}
.list a.file:hover {
color: var(--orange);
background-color: var(--platinum);
transition: background-color 0.5s, color 0.5s;
}
@media (prefers-color-scheme: dark) {
body, h1 a { background-color: black; }
.grid-lg a {
color: black;
}
.grid-lg a:visited, .grid-lg a {
color: black
}
.grid-lg a:hover {
color: lightgray;
background-color: darkgray;
transition: background-color 0.5s;
}
}
@media only screen and (max-width: 1170px) {
.grid-lg {
display: block;
}
.grid-lg a {
margin: 10px;
}
.list, form {
width: 90%;
}
.list a img {
display: none;
}
}
input[type="file"] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
input[type="file"] + label, input[type="submit"], button {
color: var(--orange);
padding: 10px;
font-size: 1.25em;
font-weight: 700;
display: inline-block;
border: 2px solid var(--orange);
border-radius: 5px;
transition: background-color 0.5s, color 0.5s;
}
input[type="text"] {
padding: 10px;
font-size: 1.25em;
font-weight: 500;
display: inline-block;
border: 2px solid var(--orange);
border-radius: 5px;
transition: background-color 0.5s, color 0.5s;
width: 50%;
}
input[type="file"]:focus + label,
input[type="file"] + label:hover,
input[type="submit"]:hover,
button:hover {
background-color: var(--orange);
color: var(--white);
transition: background-color 0.5s, color 0.5s;
}
progress {
background: var(--platinum);
border: 1px solid var(--orange);
}
progress::-webkit-progress-bar {
background: var(--orange);
}
progress::-webkit-progress-value {
background: var(--orange);
}
progress::-moz-progress-bar {
background: var(--orange);
}
button {
padding: 1vh;
font-size: 24px;
font-weight: bold;
text-decoration: none;
text-align: center;
transition: background-color 0.5s;
margin: 5px 0;
position: relative;
border-radius: 0 5px 5px 0;
}
.directory ~ button {
border-color: var(--blue);
}
.directory ~ button:hover {
background-color: var(--blue);
}
.item, .forms {
display: flex;
}
.item {
justify-content: center;
}
.forms {
width: 40%;
margin: 0 auto;
justify-content: space-around;
}

View file

@ -0,0 +1,23 @@
SVG Icons provided by Paweł Kuna: https://github.com/tabler/tabler-icons under the MIT License
MIT License
Copyright (c) 2020 Paweł Kuna
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-file" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="white" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<polyline points="14 3 14 8 19 8" />
<path d="M17 21H7a2 2 0 0 1 -2 -2V5a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-folder" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="rgba(244, 244, 248, 1)" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2" />
</svg>

After

Width:  |  Height:  |  Size: 386 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-trash" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="4" y1="7" x2="20" y2="7" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>

After

Width:  |  Height:  |  Size: 529 B

View file

@ -1 +1,19 @@
<!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.ab401df5.chunk.js"></script><script src="/static/js/main.33319914.chunk.js"></script></body></html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Sliproad</title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<h1><a href="/">Sliproad</a></h1>
<main id="main">
</main>
<script src="/javascript/app.js"></script>
</body>
</html>

View file

@ -0,0 +1,159 @@
// Register our router, and fire it off initially in case user is being linked a dir.
window.addEventListener("hashchange", router, false);
router()
let fileinput = ""
// Fetch file listing for a provider and optional path.
function getFileListing(provider, path = "") {
// There is some funky behaviour happening here between localhost and a deployed instance.
// This *fixes* is, but it's not ideal.
if (!path.startsWith("/") && path !== "") {
path = "/" + path
}
fetch(`/api/files/${provider}${path}`)
.then((response) => {
return response.json()
})
.then((data) => {
let files = data["Files"]
if (!files) {
files = []
}
html`
<div class="forms">
<form id="uploadfile" action="#" method="post">
<input type="file" id="file" data-dir="${provider}${path}"><label for="file">Upload</label>
<progress id="progress" value="0" max="100" hidden=""></progress>
</form>
<form id="createdir" action="#" method="post">
<input type="text" id="newdir" data-dir="${provider}${path}">
<input type="submit" value="Create Directory" id="newdir_submit">
</form>
</div>
<div class="list">
${files.map(file =>
`<div class="item"><a class="${file.IsDirectory ? "directory" : "file"}" href="${!file.IsDirectory ? `/api/files/${provider}${path}/${file.Name}` : `#${provider}${path === "" ? "" : path}/${file.Name}`}">
<span>${file.IsDirectory ? '<img src="/icons/folder.svg"/>' : '<img src="/icons/file.svg"/>'}${file.Name}</span>
</a><button onclick="deleteFile('${provider}', '${path === "" ? '' : path}', '${file.Name}')"><img src="/icons/trash.svg"/></button></div>
`
).join('')}
</div>
`
// Register our new listeners for uploading files.
fileinput = document.getElementById("file")
fileinput.addEventListener("change", onSelectFile, false)
createdir = document.getElementById("createdir")
createdir.addEventListener("submit", mkdir)
})
}
function deleteFile(provider, path, filename) {
let xhrObj = new XMLHttpRequest()
let rp = `${provider}${path === "" ? "" : path}/${filename}`
xhrObj.addEventListener("loadend", uploadFinish, false)
xhrObj.open("DELETE", `/api/files/${rp}`, true)
xhrObj.send()
}
// Fetch list of providers and render.
function getProviders() {
fetch(`/api/providers`)
.then((response) => {
return response.json()
})
.then((data) => {
let providers = data
html`
<div class="grid-lg">
${providers.map(provider =>
`<a href=#${provider}>
${provider}
</a>
`
).join('')}
</div>
`
})
}
// Dumb router function for passing around values from the hash.
function router(event = null) {
let hash = location.hash.replace("#", "")
// If hash is empty, "redirect" to index.
if (hash === "") {
getProviders()
return
}
let path = hash.split("/")
let provider = path.shift()
path = path.join("/")
getFileListing(provider, path)
}
function mkdir(event) {
event.preventDefault()
let xhrObj = new XMLHttpRequest()
mkdir = document.getElementById("newdir")
let path = mkdir.getAttribute("data-dir")
let mkdirvalue = mkdir.value
xhrObj.addEventListener("loadend", uploadFinish, false)
xhrObj.open("POST", `/api/files/${path}/${mkdirvalue}`, true)
xhrObj.setRequestHeader("X-NAS-Type", "directory")
xhrObj.send()
}
// File upload functions. Uses XMLHttpRequest so we can display file upload progress.
function onSelectFile() {
upload(fileinput.getAttribute("data-dir"), fileinput.files[0])
}
function upload(path, file) {
let xhrObj = new XMLHttpRequest()
let formData = new FormData()
formData.append("file", file)
xhrObj.upload.addEventListener("loadstart", uploadStarted, false)
xhrObj.upload.addEventListener("progress", uploadProgress, false)
xhrObj.upload.addEventListener("loadend", uploadFinish, false)
xhrObj.open("POST", `/api/files/${path}`, true)
xhrObj.send(formData)
}
function uploadStarted(e) {
document.getElementById("progress").hidden = false
}
function uploadProgress(e) {
let progressBar = document.getElementById("progress")
progressBar.max = e.total
progressBar.value = e.loaded
}
function uploadFinish(e) { router() }
// Tagged template function for parsing a string of text as HTML objects
// <3 @innovati for this brilliance.
function html(strings, ...things) {
// Our "body", where we'll render stuff.
const body = document.getElementById("main")
let x = document.createRange().createContextualFragment(
strings.reduce(
(markup, string, index) => {
markup += string
if (things[index]) {
markup += things[index]
}
return markup
},
''
)
)
body.innerHTML = ""
body.append(x)
}

View file

@ -1,277 +0,0 @@
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,
}
// Insert token into database.
db, err := sql.Open("sqlite3", "assets/config/users.db")
defer db.Close()
if err != nil {
return nil, err
}
statement, err := db.Prepare("UPDATE users SET token=? WHERE username=?")
if err != nil {
return nil, err
}
_, err = statement.Exec(cookieStr, u.Username)
if err != nil {
return nil, err
}
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))
}

View file

@ -0,0 +1,28 @@
package authentication
import (
"fmt"
"github.com/Nerzal/gocloak/v5"
)
// AuthConfig contains the configuration for the IdP.
var AuthConfig map[string]string
// HasAuth checks the passed token against the IdP, and returns true
// if the IdP can return the user's info, false if not.
func HasAuth(accessToken string) (success bool) {
client := gocloak.NewClient(AuthConfig["provider_url"])
_, err := client.GetUserInfo(accessToken, AuthConfig["realm"])
if err != nil {
return false
}
return true
}
// GetLoginLink generates a redirect link to the IdP login page.
func GetLoginLink() (url string) {
baseString := "%v/auth/realms/%v/protocol/openid-connect/auth?client_id=account&response_mode=fragment&response_type=token&login=true&redirect_uri=%v/api/auth/callback"
authURL := fmt.Sprintf(baseString, AuthConfig["provider_url"], AuthConfig["realm"], AuthConfig["redirect_base_url"])
return authURL
}

View file

@ -1 +0,0 @@
env GOOS=linux GOARCH=arm GOARM=5 go build

View file

@ -1,66 +0,0 @@
package common
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
)
// Handler is the signature of HTTP Handler that is passed to Handle function
type Handler func(rc *RouterContext, w http.ResponseWriter, r *http.Request) *HTTPError
// HTTPError is any error that occurs in middlewares or the code that handles HTTP Frontend of application
// Message is logged to console and Status Code is sent in response
// If a Middleware sends an HTTPError, No middlewares further up in chain are executed
type HTTPError struct {
// Message to log in console
Message string
// Status code that'll be sent in response
StatusCode int
}
// RouterContext contains any information to be shared with middlewares.
type RouterContext struct {
User *User
}
// User struct denotes the data is stored in the cookie
type User struct {
Username string `json:"username"`
}
// ReadAndServeFile reads the file from specified location and sends it in response
func ReadAndServeFile(name string, w http.ResponseWriter) *HTTPError {
f, err := os.Open(name)
if err != nil {
if os.IsNotExist(err) {
return &HTTPError{
Message: fmt.Sprintf("%s not found", name),
StatusCode: http.StatusNotFound,
}
}
return &HTTPError{
Message: fmt.Sprintf("error in reading %s: %v\n", name, err),
StatusCode: http.StatusInternalServerError,
}
}
defer f.Close()
stats, err := f.Stat()
if err != nil {
log.Printf("error in fetching %s's stats: %v\n", name, err)
} else {
w.Header().Add("Content-Length", strconv.FormatInt(stats.Size(), 10))
}
_, err = io.Copy(w, f)
if err != nil {
log.Printf("error in copying %s to response: %v\n", name, err)
}
return nil
}

276
files/backblaze.go Normal file
View file

@ -0,0 +1,276 @@
package files
import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
)
type BackblazeProvider struct {
FileProvider
Bucket string
DownloadLocation string
}
type BackblazeAuthPayload struct {
AccountId string `json:"accountId"`
AuthToken string `json:"authorizationToken"`
ApiUrl string `json:"apiUrl"`
DownloadUrl string `json:"downloadUrl"`
}
type BackblazeFile struct {
Action string `json:"action"`
Size int64 `json:"contentLength"`
Type string `json:"contentType"`
FileName string `json:"fileName"`
Timestamp int64 `json:"uploadTimestamp"`
}
type BackblazeFilePayload struct {
Files []BackblazeFile `json:"files"`
}
type BackblazeBucketInfo struct {
BucketId string `json:"bucketId"`
BucketName string `json:"bucketName"`
}
type BackblazeBucketInfoPayload struct {
Buckets []BackblazeBucketInfo `json:"buckets"`
}
type BackblazeUploadInfo struct {
UploadUrl string `json:"uploadUrl"`
AuthToken string `json:"authorizationToken"`
}
// Call Backblaze API endpoint to authorize and gather facts.
func (bp *BackblazeProvider) Setup(args map[string]string) bool {
applicationKeyId := args["applicationKeyId"]
applicationKey := args["applicationKey"]
client := &http.Client{}
req, err := http.NewRequest("GET",
"https://api.backblazeb2.com/b2api/v2/b2_authorize_account",
nil)
if err != nil {
return false
}
req.SetBasicAuth(applicationKeyId, applicationKey)
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return false
}
var data BackblazeAuthPayload
err = json.Unmarshal(body, &data)
if err != nil {
return false
}
bp.Authentication = data.AuthToken
bp.Location = data.ApiUrl
bp.Name = data.AccountId
bp.DownloadLocation = data.DownloadUrl
return true
}
func (bp *BackblazeProvider) GetDirectory(path string) Directory {
client := &http.Client{}
requestBody := fmt.Sprintf(`{"bucketId": "%s"}`, bp.Bucket)
req, err := http.NewRequest("POST",
bp.Location + "/b2api/v2/b2_list_file_names",
bytes.NewBuffer([]byte(requestBody)))
if err != nil {
fmt.Println(err.Error())
return Directory{}
}
req.Header.Add("Authorization", bp.Authentication)
resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return Directory{}
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
return Directory{}
}
var data BackblazeFilePayload
err = json.Unmarshal(body, &data)
if err != nil {
fmt.Println(err.Error())
return Directory{}
}
finalDir := Directory{
Path: bp.Bucket,
}
for _, v := range data.Files {
file := FileInfo{
IsDirectory: v.Action == "folder",
Name: v.FileName,
}
if v.Action != "folder" {
split := strings.Split(v.FileName, ".")
file.Extension = split[len(split) - 1]
}
finalDir.Files = append(finalDir.Files, file)
}
return finalDir
}
func (bp *BackblazeProvider) FilePath(path string) string {
return ""
}
func (bp *BackblazeProvider) RemoteFile(path string, w io.Writer) {
client := &http.Client{}
// Get bucket name >:(
bucketIdPayload := fmt.Sprintf(`{"accountId": "%s", "bucketId": "%s"}`, bp.Name, bp.Bucket)
req, err := http.NewRequest("POST", bp.Location + "/b2api/v2/b2_list_buckets",
bytes.NewBuffer([]byte(bucketIdPayload)))
if err != nil {
fmt.Println(err.Error())
return
}
req.Header.Add("Authorization", bp.Authentication)
res, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return
}
bucketData, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err.Error())
return
}
var data BackblazeBucketInfoPayload
json.Unmarshal(bucketData, &data)
ourBucket := data.Buckets[0].BucketName
// Get file and write over to client.
url := strings.Join([]string{bp.DownloadLocation,"file",ourBucket,path}, "/")
req, err = http.NewRequest("GET",
url,
bytes.NewBuffer([]byte("")))
if err != nil {
fmt.Println(err.Error())
return
}
req.Header.Add("Authorization", bp.Authentication)
file, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return
}
_, err = io.Copy(w, file.Body)
if err != nil {
fmt.Println(err.Error())
return
}
}
func (bp *BackblazeProvider) SaveFile(file io.Reader, filename string, path string) bool {
client := &http.Client{}
bucketIdPayload := fmt.Sprintf(`{"bucketId": "%s"}`, bp.Bucket)
req, err := http.NewRequest("POST", bp.Location + "/b2api/v2/b2_get_upload_url",
bytes.NewBuffer([]byte(bucketIdPayload)))
if err != nil {
fmt.Println(err.Error())
return false
}
req.Header.Add("Authorization", bp.Authentication)
res, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return false
}
bucketData, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err.Error())
return false
}
var data BackblazeUploadInfo
err = json.Unmarshal(bucketData, &data)
if err != nil {
fmt.Println(err.Error())
return false
}
req, err = http.NewRequest("POST",
data.UploadUrl,
file,
)
if err != nil {
fmt.Println(err.Error())
return false
}
// Read the content
var bodyBytes []byte
if req.Body != nil {
bodyBytes, _ = ioutil.ReadAll(req.Body)
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// Calculate SHA1 and add required headers.
fileSha := sha1.New()
fileSha.Write(bodyBytes)
req.Header.Add("Authorization", data.AuthToken)
req.Header.Add("X-Bz-File-Name", filename)
req.Header.Add("Content-Type", "b2/x-auto")
req.Header.Add("X-Bz-Content-Sha1", fmt.Sprintf("%x", fileSha.Sum(nil)))
req.ContentLength = int64(len(bodyBytes))
res, err = client.Do(req)
if err != nil {
fmt.Println(err.Error())
return false
}
return true
}
func (bp *BackblazeProvider) ObjectInfo(path string) (bool, bool, string) {
// B2 is really a "flat" filesystem, with directories being virtual.
// Therefore, we can assume everything is a file ;)
// TODO: Return true value.
isDir := path == ""
return true, isDir, FileIsRemote
}
func (bp *BackblazeProvider) CreateDirectory(path string) bool {
// See above comment about virtual directories.
return false
}
func (bp *BackblazeProvider) Delete(path string) bool {
// TODO
return false
}

103
files/disk.go Normal file
View file

@ -0,0 +1,103 @@
package files
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
)
type DiskProvider struct{
FileProvider
}
func (dp *DiskProvider) Setup(args map[string]string) bool {
return true
}
func (dp *DiskProvider) GetDirectory(path string) Directory {
rp := strings.Join([]string{dp.Location,path}, "/")
fileDir, err := ioutil.ReadDir(rp)
if err != nil {
_ = os.MkdirAll(path, 0644)
}
var fileList []FileInfo
for _, file := range fileDir {
info := FileInfo{
IsDirectory: file.IsDir(),
Name: file.Name(),
}
if !info.IsDirectory {
split := strings.Split(file.Name(), ".")
info.Extension = split[len(split) - 1]
}
fileList = append(fileList, info)
}
return Directory{
Path: rp,
Files: fileList,
}
}
func (dp *DiskProvider) FilePath(path string) string {
rp := strings.Join([]string{dp.Location,path}, "/")
return rp
}
func (dp *DiskProvider) RemoteFile(path string, writer io.Writer) {
return
}
func (dp *DiskProvider) SaveFile(file io.Reader, filename string, path string) bool {
rp := strings.Join([]string{dp.Location,path,filename}, "/")
f, err := os.OpenFile(rp, os.O_WRONLY|os.O_CREATE, 0644)
if err != nil {
fmt.Printf("error creating %v: %v\n", rp, err.Error())
return false
}
defer f.Close()
_, err = io.Copy(f, file)
if err != nil {
fmt.Printf("error writing %v: %v\n", rp, err.Error())
return false
}
return true
}
func (dp *DiskProvider) ObjectInfo(path string) (bool, bool, string) {
rp := strings.Join([]string{dp.Location,path}, "/")
fileStat, err := os.Stat(rp)
if err != nil {
fmt.Printf("error gather stats for file %v: %v", rp, err.Error())
return false, false, FileIsLocal
}
if fileStat.IsDir() {
return true, true, FileIsLocal
}
return true, false, FileIsLocal
}
func (dp *DiskProvider) CreateDirectory(path string) bool {
rp := strings.Join([]string{dp.Location,path}, "/")
err := os.Mkdir(rp, 0755)
if err != nil {
fmt.Printf("Error creating directory %v: %v\n", rp, err.Error())
return false
}
return true
}
func (dp *DiskProvider) Delete(path string) bool {
rp := strings.Join([]string{dp.Location,path}, "/")
err := os.RemoveAll(rp)
if err != nil {
fmt.Printf("Error removing %v: %v\n", path, err.Error())
return false
}
return true
}

91
files/fileprovider.go Normal file
View file

@ -0,0 +1,91 @@
package files
import (
"io"
)
// FileIsRemote denotes whether file is a remote file.
const FileIsRemote = "remote"
// FileIsLocal denotes whether file is a local file.
const FileIsLocal = "local"
// FileProvider aggregates some very basic properties for authentication and
// provider decoding.
type FileProvider struct {
Name string `yaml:"name"`
Provider string `yaml:"provider"`
Authentication string `yaml:"authentication"`
Location string `yaml:"path"`
Config map[string]string `yaml:"config"`
}
// Directory contains the path and a collection of FileInfos.
type Directory struct {
Path string
Files []FileInfo
}
// FileInfo describes a single file or directory, doing it's best to
// figure out the extension as well.
type FileInfo struct {
IsDirectory bool
Name string
Extension string
}
// FileProviderInterface provides some sane default functions.
type FileProviderInterface interface {
Setup(args map[string]string) (ok bool)
GetDirectory(path string) (directory Directory)
FilePath(path string) (realpath string)
RemoteFile(path string, writer io.Writer)
SaveFile(file io.Reader, filename string, path string) (ok bool)
ObjectInfo(path string) (exists bool, isDir bool, location string)
CreateDirectory(path string) (ok bool)
Delete(path string) (ok bool)
}
/** DO NOT USE THESE DEFAULTS **/
// Setup runs when the application starts up, and allows for things like authentication.
func (f FileProvider) Setup(args map[string]string) bool {
return false
}
// GetDirectory fetches a directory's contents.
func (f FileProvider) GetDirectory(path string) Directory {
return Directory{}
}
// FilePath returns the path to the file, whether it be a URL or local file path.
func (f FileProvider) FilePath(path string) string {
return ""
}
// RemoteFile will bypass http.ServeContent() and instead write directly to the response.
func (f FileProvider) RemoteFile(path string, writer io.Writer) {
return
}
// SaveFile will save a file with the contents of the io.Reader at the path specified.
func (f FileProvider) SaveFile(file io.Reader, filename string, path string) bool {
return false
}
// ObjectInfo will return the info for an object given a path to if the file exists and location.
// Should return whether the path exists, if the path is a directory, and if it lives on disk.
// (see constants defined: `FILE_IS_REMOTE` and `FILE_IS_LOCAL`)
func (f FileProvider) ObjectInfo(path string) (bool, bool, string) {
return false, false, ""
}
// CreateDirectory will create a directory on services that support it.
func (f FileProvider) CreateDirectory(path string) bool {
return false
}
// Delete simply deletes a file. This is expected to be a destructive action by default.
func (f FileProvider) Delete(path string) bool {
return false
}

View file

@ -1,194 +0,0 @@
package files
import (
"encoding/json"
"fmt"
"github.com/gmemstr/nas/auth"
"github.com/gmemstr/nas/common"
"github.com/gorilla/mux"
"io"
"io/ioutil"
"net/http"
"os"
"strings"
)
type Config struct {
ColdStorage string
HotStorage string
}
type Directory struct {
Path string
Files []FileInfo
Previous string
Prefix string
SinglePrefix string
}
type FileInfo struct {
IsDirectory bool
Name string
}
func GetUserDirectory(r *http.Request, tier string) (string, string, string) {
usr, err := auth.DecryptCookie(r)
if err != nil {
return "", "", ""
}
username := usr.Username
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
panic(err)
}
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage := config.HotStorage + username
prefix := "files"
singleprefix := "file"
if tier == "cold" {
storage = config.ColdStorage + username
prefix = "archive"
singleprefix = "archived"
}
// Ensure directory exists.
_, err = ioutil.ReadDir(storage)
if err != nil && storage == "" {
fmt.Println(storage)
_ = os.MkdirAll(storage, 0644)
}
return storage, prefix, singleprefix
}
// 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,
}
}
path := storage
if id != "" {
path = storage + id
}
fileDir, err := ioutil.ReadDir(path)
if err != nil && path == "" {
fmt.Println(path)
_ = os.MkdirAll(path, 0644)
}
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.
previous := strings.Split(path, "/")
previous = previous[:len(previous)-1]
previousPath := strings.Join(previous, "/")
directory := Directory{
Path: path,
Files: fileList,
Previous: previousPath,
Prefix: prefix,
SinglePrefix: singleprefix,
}
resultJson, err := json.Marshal(directory);
w.Write(resultJson);
return nil;
}
}
// Lists out directory using template.
func ViewFile() 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"]
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
panic(err)
}
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
// Default to hot storage
storage, _, _ := GetUserDirectory(r, tier)
path := storage + "/" + id
common.ReadAndServeFile(path, w)
return nil
}
}
func UploadFile() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
d, err := ioutil.ReadFile("assets/config/config.json")
vars := mux.Vars(r)
tier := vars["tier"]
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
err = r.ParseMultipartForm(32 << 20)
path := strings.Join(r.Form["path"], "")
// Default to hot storage
storage, _, _ := GetUserDirectory(r, tier)
file, handler, err := r.FormFile("file")
if err != nil {
fmt.Println(err)
return nil
}
defer file.Close()
f, err := os.OpenFile(storage+"/"+path+"/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
panic(err)
}
defer f.Close()
io.Copy(f, file)
return nil
}
}

117
files/files_test.go Normal file
View file

@ -0,0 +1,117 @@
package files
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"reflect"
"testing"
)
const DISK_TESTING_GROUNDS = ".testing_data_directory"
// Test basic file provider.
// Should return, essentially, nothing. Can be used as a template for other providers.
func TestFileProvider(t *testing.T) {
fp := FileProvider{}
setup := fp.Setup(map[string]string{}); if setup != false {
t.Errorf("Default FileProvider Setup() returned %v, expected false.", setup)
}
getdirectory := fp.GetDirectory(""); if len(getdirectory.Files) != 0 {
t.Errorf("Default FileProvider GetDirectory() files returned %v, expected none.", getdirectory.Files)
}
filepath := fp.FilePath(""); if filepath != "" {
t.Errorf("Default FileProvider FilePath() %v, expected nothing.", filepath)
}
savefile := fp.SaveFile(nil, "", ""); if savefile != false {
t.Errorf("Default FileProvider SaveFile() attempted to save a file.")
}
exists, isDir, location := fp.ObjectInfo(""); if exists || isDir || location != "" {
t.Errorf("Default FileProvider ObjectInfo() did not return default empty values.")
}
createdirectory := fp.CreateDirectory(""); if createdirectory {
t.Errorf("Default FileProvider CreateDirectory() returned %v, expected false.", createdirectory)
}
delete := fp.Delete(""); if delete {
t.Errorf("Default FileProvider Delete() returned %v, expected false.", createdirectory)
}
}
// Test functions provided by fileutils, which do not return anything.
func TestFileUtils(t *testing.T) {
ProviderConfig = map[string]FileProvider{
"test": {
Name: "test_disk",
Provider: "disk",
Location: DISK_TESTING_GROUNDS,
},
}
var i FileProviderInterface
TranslateProvider("test", &i)
typeof := reflect.TypeOf(i); if typeof != (reflect.TypeOf(&DiskProvider{})) {
t.Errorf("TranslateProvider() returned %v, expected DiskProvider{}", typeof)
}
SetupProviders()
if Providers["test"] == nil {
t.Errorf("SetupProviders() did not setup test provider")
}
}
func TestDiskProvider(t *testing.T) {
// Initialize testing data
t.Cleanup(DiskProviderTestCleanup)
err := os.Mkdir(DISK_TESTING_GROUNDS, 0755)
if err != nil {
t.Fatalf("Failed to create testing directory for DiskProvider: %v", err.Error())
}
err = ioutil.WriteFile(DISK_TESTING_GROUNDS + "/testing.txt", []byte("testing file!"), 0755)
if err != nil {
t.Fatalf("Failed to create testing file for DiskProvider: %v", err.Error())
}
dp := DiskProvider{FileProvider{Location: DISK_TESTING_GROUNDS}}
setup := dp.Setup(map[string]string{}); if setup != true {
t.Errorf("DiskProvider Setup() returned %v, expected true.", setup)
}
getdirectory := dp.GetDirectory(""); if len(getdirectory.Files) != 1 {
t.Errorf("DiskProvider GetDirectory() files returned %v, expected 1.", getdirectory.Files)
}
filepath := dp.FilePath("testing.txt"); if filepath != DISK_TESTING_GROUNDS + "/testing.txt"{
t.Errorf("DiskProvider FilePath() returned %v, expected path.", filepath)
}
testfile := bytes.NewReader([]byte("second test file!"))
savefile := dp.SaveFile(testfile, "second_test.txt", ""); if savefile != true {
t.Errorf("DiskProvider SaveFile() could not save a file.")
}
exists, isDir, location := dp.ObjectInfo("second_test.txt"); if !exists || isDir || location != "local" {
t.Errorf("DiskProvider ObjectInfo() returned %v %v %v, expected true, false, local", exists, isDir, location)
}
createdirectory := dp.CreateDirectory("test_dir"); if !createdirectory {
t.Errorf("DiskProvider CreateDirectory() returned %v, expected true.", createdirectory)
}
delete := dp.Delete("test_dir"); if !delete {
t.Errorf("DiskProvider Delete() returned %v, expected true.", createdirectory)
}
}
func DiskProviderTestCleanup() {
err := os.RemoveAll(DISK_TESTING_GROUNDS)
if err != nil {
fmt.Printf("Unable to remove testing directory %v, please manully remove it", DISK_TESTING_GROUNDS)
}
}

36
files/fileutils.go Normal file
View file

@ -0,0 +1,36 @@
package files
import "fmt"
var ProviderConfig map[string]FileProvider
var Providers map[string]*FileProviderInterface
func TranslateProvider(codename string, i *FileProviderInterface) {
provider := ProviderConfig[codename]
if provider.Provider == "disk" {
*i = &DiskProvider{provider,}
return
}
if provider.Provider == "backblaze" {
bbProv := &BackblazeProvider{provider, provider.Config["bucket"], ""}
*i = bbProv
return
}
*i = FileProvider{}
}
func SetupProviders() {
Providers = make(map[string]*FileProviderInterface)
for name, provider := range ProviderConfig {
var i FileProviderInterface
TranslateProvider(name, &i)
success := i.Setup(provider.Config)
if !success {
fmt.Printf("%s failed to initialize\n", name)
} else {
Providers[name] = &i
fmt.Printf("%s initialized successfully\n", name)
}
}
}

23
frontend/.gitignore vendored
View file

@ -1,23 +0,0 @@
# 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*

View file

@ -1,68 +0,0 @@
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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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

13307
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,32 +0,0 @@
{
"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"
]
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,38 +0,0 @@
<!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>UniStorage</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>

View file

@ -1,15 +0,0 @@
{
"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"
}

View file

@ -1,72 +0,0 @@
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);
}
}

View file

@ -1,204 +0,0 @@
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 path="/hot/:dir?" component={HotFileListing} />
<Route path="/cold/:dir?" component={ColdFileListing} />
</div>
</BrowserRouter>
);
}
}
class Login extends Component {
render() {
return (
<div className="LoginForm">
<form method="POST">
<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,
directory: "",
};
};
componentDidMount() {
const { match: { params } } = this.props;
this.setState({directory: params.dir});
this.loadFileListing(params.dir);
}
componentDidUpdate(prevProps, prevState) {
const { match: { params } } = this.props;
console.log("Prev state: ", prevState);
if (prevState.directory != params.dir) {
this.setState({directory: params.dir});
this.loadFileListing(params.dir);
}
}
loadFileListing(dir) {
if (dir != undefined) {
fetch("/api/hot/" + dir)
.then(response => response.json())
.then(data => this.setState({ files: data, loading: false }));
}
else {
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>;
}
if (!loading && !this.state.files.Files) {
return (
<div>
<FileUploadForm tier="hot" />
Empty
</div>
)
}
return (
<div>
<FileUploadForm tier="hot" />
<FileList files={this.state.files.Files} tier="hot" />
</div>
)
}
}
class ColdFileListing extends HotFileListing {
loadFileListing(dir) {
if (dir != undefined) {
fetch("/api/cold/" + dir)
.then(response => response.json())
.then(data => this.setState({ files: data, loading: false }));
}
else {
fetch("/api/cold/")
.then(response => response.json())
.then(data => this.setState({ files: data, loading: false }));
}
}
}
class FileUploadForm extends Component {
render() {
return (
<form className="FileUpload" enctype="multipart/form-data" method="POST" action={`/api/upload/${this.props.tier}`}>
<input type="file" name="file" id="file" />
<input type="submit" value="Upload" />
</form>
)
}
}
class FileList extends Component {
render () {
console.log(this.props.files);
let fileComponents = this.props.files.map((file) => {
console.log(file)
if (file.IsDirectory) {
return <Directory dir={file} tier={this.props.tier} />
}
return <File file={file} tier={this.props.tier} />
})
return (
<div>
<ul>{fileComponents}</ul>
</div>
)
}
}
class Directory extends Component {
render() {
return (
<div>
<Link to={`/${this.props.tier}/${this.props.dir.Name}`}>{this.props.dir.Name}/</Link>
</div>
)
}
}
class File extends Component {
render() {
return (
<div>
<a href={`/api/${this.props.tier}/file/${this.props.file.Name}`}>{this.props.file.Name}</a>
</div>
)
}
}
export default App;

View file

@ -1,9 +0,0 @@
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);
});

View file

@ -1,13 +0,0 @@
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;
}

View file

@ -1,12 +0,0 @@
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();

View file

@ -1,7 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,135 +0,0 @@
// 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();
});
}
}

File diff suppressed because it is too large Load diff

12
go.mod
View file

@ -1,10 +1,12 @@
module gmem.ca/nas
module github.com/gmemstr/nas
go 1.13
require (
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722
github.com/gorilla/mux v1.7.4 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
github.com/Nerzal/gocloak/v5 v5.1.0
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/gorilla/mux v1.7.4
github.com/kr/pretty v0.2.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
)

40
go.sum
View file

@ -1,13 +1,37 @@
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722 h1:LmCeaQfQHTfKtx1HbR9cHMndomz14InE0d5lBWp/opI=
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722/go.mod h1:hv1O7aXobFKTuw2JKHkzEnFMdQqv6wcLEgoTD7mxIY4=
github.com/Nerzal/gocloak/v5 v5.1.0 h1:1YP4+GoY1DZ1k7WyNNr8xbFyt55B9ORn2ZHu+XqUK0Q=
github.com/Nerzal/gocloak/v5 v5.1.0/go.mod h1:8v53okuWiWXOKOS6qil8cOn7+5JSQfX1t1d+Nj8FpYk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/go-resty/resty/v2 v2.0.0 h1:9Nq/U+V4xsoDnDa/iTrABDWUCuk3Ne92XFHPe6dKWUc=
github.com/go-resty/resty/v2 v2.0.0/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

57
router/authrouter.go Normal file
View file

@ -0,0 +1,57 @@
package router
import (
"fmt"
"io/ioutil"
"net/http"
"github.com/gmemstr/nas/authentication"
)
// AuthEnabled is a global variable that determines whether we were
// able to set up an authentication method (e.g Keycloak).
var AuthEnabled bool = true
func requiresAuth() handler {
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
if !AuthEnabled {
return nil
}
cookie, err := r.Cookie("NAS-SESSION")
if err != nil || !authentication.HasAuth(cookie.Value) {
if err != nil {
fmt.Println("Error", err.Error())
}
http.Redirect(w, r, authentication.GetLoginLink(), 307)
return &httpError{
Message: "Unauthorized! Redirecting to /login",
StatusCode: http.StatusTemporaryRedirect,
}
}
return nil
}
}
func callbackAuth() handler {
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
// Translate callback GET to POST to set cookie, then redirect.
if r.Method == "GET" {
javascript := `
<script>fetch("/api/auth/callback", {method:"POST", body: window.location.hash.split("&")[1].split("=")[1]}).then((r) => window.location.href = "/")</script>`
w.Write([]byte(javascript))
return nil
}
token, _ := ioutil.ReadAll(r.Body)
// Set as HttpOnly cookie to mitigate XSS risk.
jwtCookie := http.Cookie{Name: "NAS-SESSION",
Value: string(token),
HttpOnly: true,
Path: "/",
}
http.SetCookie(w, &jwtCookie)
return nil
}
}

147
router/filerouter.go Normal file
View file

@ -0,0 +1,147 @@
package router
import (
"encoding/json"
"fmt"
"github.com/gmemstr/nas/files"
"github.com/gorilla/mux"
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"
)
func handleProvider() handler {
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
vars := mux.Vars(r)
providerCodename := vars["provider"]
providerCodename = strings.Replace(providerCodename, "/", "", -1)
provider := *files.Providers[providerCodename]
// Directory listing or serve file.
if r.Method == "GET" {
filename, err := url.QueryUnescape(vars["file"])
if err != nil {
return &httpError{
Message: fmt.Sprintf("error determining filetype for %s\n", filename),
StatusCode: http.StatusInternalServerError,
}
}
ok, isDir, location := provider.ObjectInfo(filename)
if !ok {
return &httpError{
Message: fmt.Sprintf("error locating file %s\n", filename),
StatusCode: http.StatusNotFound,
}
}
if isDir {
fileList := provider.GetDirectory(filename)
data, err := json.Marshal(fileList)
if err != nil {
return &httpError{
Message: fmt.Sprintf("error fetching filelisting for %s\n", vars["file"]),
StatusCode: http.StatusNotFound,
}
}
w.Write(data)
return nil
}
// If the file is local, attempt to use http.ServeContent for correct headers.
if location == files.FileIsLocal {
rp := provider.FilePath(filename)
if rp != "" {
f, err := os.Open(rp)
if err != nil {
return &httpError{
Message: fmt.Sprintf("error opening file %s\n", rp),
StatusCode: http.StatusInternalServerError,
}
}
http.ServeContent(w, r, filename, time.Time{}, f)
}
}
// If the file is remote, then delegate the writing to the response to the provider.
// This isn't a great workaround, but avoids caching the whole file in mem or on disk.
if location == files.FileIsRemote {
provider.RemoteFile(filename, w)
}
return nil
}
// File upload or directory creation.
if r.Method == "POST" {
xType := r.Header.Get("X-NAS-Type")
// We only really care about this header of creating a directory.
if xType == "directory" {
dirname := vars["file"]
success := provider.CreateDirectory(dirname)
if !success {
return &httpError{
Message: fmt.Sprintf("error creating directory %s\n", dirname),
StatusCode: http.StatusInternalServerError,
}
}
_, _ = w.Write([]byte("created directory"))
return nil
}
err := r.ParseMultipartForm(32 << 20)
if err != nil {
return &httpError{
Message: fmt.Sprintf("error parsing form for %s\n", vars["file"]),
StatusCode: http.StatusInternalServerError,
}
}
file, handler, err := r.FormFile("file")
defer file.Close()
success := provider.SaveFile(file, handler.Filename, vars["file"])
if !success {
return &httpError{
Message: fmt.Sprintf("error saving file %s\n", vars["file"]),
StatusCode: http.StatusInternalServerError,
}
}
_, _ = w.Write([]byte("saved file"))
}
// Delete file.
if r.Method == "DELETE" {
path := vars["file"]
success := provider.Delete(path)
if !success {
return &httpError{
Message: fmt.Sprintf("error deleting %s\n", path),
StatusCode: http.StatusInternalServerError,
}
}
_, _ = w.Write([]byte("deleted"))
}
return nil
}
}
func listProviders() handler {
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
var providers []string
for v := range files.ProviderConfig {
providers = append(providers, v)
}
sort.Strings(providers)
data, err := json.Marshal(providers)
if err != nil {
return &httpError{
Message: fmt.Sprintf("error provider listing"),
StatusCode: http.StatusInternalServerError,
}
}
_, _ = w.Write(data)
return nil
}
}

View file

@ -1,189 +1,110 @@
package router
import (
"database/sql"
"fmt"
"github.com/gmemstr/nas/auth"
"golang.org/x/crypto/bcrypt"
"io"
"log"
"net/http"
"os"
"strconv"
"github.com/gmemstr/nas/common"
"github.com/gmemstr/nas/files"
"github.com/gmemstr/nas/system"
"github.com/gorilla/mux"
)
func Handle(handlers ...common.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
type handler func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError
rc := &common.RouterContext{}
type httpError struct {
Message string
StatusCode int
}
type requestContext struct{}
// Loop through passed functions and execute them, passing through the current
// requestContext, response writer and request reader.
func handle(handlers ...handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
context := &requestContext{}
for _, handler := range handlers {
err := handler(rc, w, r)
err := handler(context, w, r)
if err != nil {
log.Printf("%v", err)
w.Write([]byte(http.StatusText(err.StatusCode)))
return
}
}
})
}
// Actual router, define endpoints here.
// Init initializes the main router and all routes for the application.
func Init() *mux.Router {
r := mux.NewRouter()
// "Static" paths
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("assets/web/static"))))
r.PathPrefix("/javascript/").Handler(http.StripPrefix("/javascript/", http.FileServer(http.Dir("assets/web/javascript"))))
r.PathPrefix("/css/").Handler(http.StripPrefix("/css/", http.FileServer(http.Dir("assets/web/css"))))
r.PathPrefix("/icons/").Handler(http.StripPrefix("/icons/", http.FileServer(http.Dir("assets/web/icons"))))
// Paths that require specific handlers
r.Handle("/", Handle(
auth.RequireAuthorization(1),
r.Handle("/", handle(
requiresAuth(),
rootHandler(),
)).Methods("GET")
r.Handle(`/login`, Handle(
loginHandler(),
)).Methods("POST", "GET")
r.Handle("/api/diskusage", Handle(
auth.RequireAuthorization(1),
system.DiskUsages(),
// File & Provider API
r.Handle("/api/providers", handle(
requiresAuth(),
listProviders(),
)).Methods("GET")
r.Handle(`/api/{tier:(?:hot|cold)}/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
auth.RequireAuthorization(1),
files.ViewFile(),
)).Methods("GET")
r.Handle(`/api/files/{provider:[a-zA-Z0-9]+\/*}`, handle(
requiresAuth(),
handleProvider(),
)).Methods("GET", "POST")
r.Handle("/api/upload/hot", Handle(
auth.RequireAuthorization(1),
files.UploadFile(),
)).Methods("POST")
r.Handle(`/api/files/{provider:[a-zA-Z0-9]+}/{file:.+}`, handle(
requiresAuth(),
handleProvider(),
)).Methods("GET", "POST", "DELETE")
r.Handle("/api/upload/{tier:(?:hot|cold)}", Handle(
auth.RequireAuthorization(1),
files.UploadFile(),
)).Methods("POST")
r.Handle("/api/{tier:(?:hot|cold)}/", Handle(
auth.RequireAuthorization(1),
files.Listing(),
)).Methods("GET")
r.Handle(`/api/{tier:(?:hot|cold)}/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
auth.RequireAuthorization(1),
files.Listing(),
)).Methods("GET")
// Auth API & Endpoints
r.Handle(`/api/auth/callback`, handle(
callbackAuth(),
)).Methods("GET", "POST")
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")
// Handles serving index page.
func rootHandler() handler {
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
f, err := os.Open("assets/web/index.html")
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in reading user database: %v", err),
return &httpError{
Message: fmt.Sprintf("error serving index page from assets/web"),
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()
defer f.Close()
stats, err := f.Stat()
if err != nil {
return &common.HTTPError{
Message: fmt.Sprintf("error in parsing form: %v", err),
StatusCode: http.StatusBadRequest,
return &httpError{
Message: fmt.Sprintf("error serving index page from assets/web"),
StatusCode: http.StatusInternalServerError,
}
} else {
w.Header().Add("Content-Length", strconv.FormatInt(stats.Size(), 10))
}
_, err = io.Copy(w, f)
if err != nil {
return &httpError{
Message: fmt.Sprintf("error serving index page from assets/web"),
StatusCode: http.StatusInternalServerError,
}
}
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 dbtoken sql.NullString
var dbperm int
for rows.Next() {
err := rows.Scan(&id, &dbun, &dbhsh, &dbtoken, &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,
}
return nil
}
}
// Handles /.
func rootHandler() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
var file string
switch r.URL.Path {
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),
StatusCode: http.StatusNotFound,
}
}
return common.ReadAndServeFile(file, w)
}
}

View file

@ -1,74 +0,0 @@
package system
import (
"encoding/json"
"github.com/gmemstr/nas/common"
"github.com/gmemstr/nas/files"
"io/ioutil"
"net/http"
"os"
"syscall"
)
type Config struct {
ColdStorage string
HotStorage string
}
type UsageStat struct {
Available int64
Free int64
Total int64
}
type UsageStats struct {
ColdStorage UsageStat
HotStorage UsageStat
}
func DiskUsages() common.Handler {
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
var statHot syscall.Statfs_t
var statCold syscall.Statfs_t
d, err := ioutil.ReadFile("assets/config/config.json")
if err != nil {
panic(err)
}
var config Config
err = json.Unmarshal(d, &config)
if err != nil {
panic(err)
}
storage, _, _ := files.GetUserDirectory(r,"hot")
err = syscall.Statfs(storage, &statHot)
if err != nil {
_ = os.MkdirAll(storage, 0644)
}
hotStats := UsageStat{
Free: statHot.Bsize * int64(statHot.Bfree),
Total: statHot.Bsize * int64(statHot.Blocks),
}
storage, _, _ = files.GetUserDirectory(r,"cold")
err = syscall.Statfs(storage, &statCold)
if err != nil {
_ = os.MkdirAll(storage, 0644)
}
coldStats := UsageStat{
Free: statCold.Bsize * int64(statCold.Bfree),
Total: statCold.Bsize * int64(statCold.Blocks),
}
usages := UsageStats{
HotStorage: hotStats,
ColdStorage: coldStats,
}
// Available blocks * size per block = available space in bytes
resultJson, err := json.Marshal(usages)
w.Write(resultJson)
return nil
}
}

View file

@ -1,92 +1,45 @@
/* webserver.go
*
* This is the webserver handler for Pogo, and handles
* all incoming connections, including authentication.
*/
package main
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"fmt"
"golang.org/x/crypto/bcrypt"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/gmemstr/nas/authentication"
"github.com/gmemstr/nas/files"
"github.com/gmemstr/nas/router"
"github.com/go-yaml/yaml"
)
// Main function that defines routes
func main() {
if _, err := os.Stat(".lock"); os.IsNotExist(err) {
createDatabase()
createLockFile()
// Initialize file providers.
file, err := ioutil.ReadFile("providers.yml")
if err != nil {
fmt.Println("Unable to read providers.yml file, does it exist?")
}
err = yaml.Unmarshal(file, &files.ProviderConfig)
if err != nil {
fmt.Println("Unable to parse providers.yml file, is it correct?")
}
files.SetupProviders()
// Initialize auth if set up.
authConfig, err := ioutil.ReadFile("auth.yml")
if err != nil {
fmt.Println("!! No Keycloack configuration found !!\n!! Requests will be unauthenticated !!")
router.AuthEnabled = false
} else {
err = yaml.Unmarshal(authConfig, &authentication.AuthConfig)
if err != nil {
fmt.Println("Unable to parse auth.yml file, is it correct?")
router.AuthEnabled = false
}
fmt.Println("Keycloak configured")
}
r := router.Init()
fmt.Println("Your NAS instance is live on port :3000")
fmt.Println("Your sliproad instance is live on port :3000")
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, `token` 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,permissions) VALUES (0,'admin','" + string(hash) + "',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
}