mirror of
https://github.com/gmemstr/sliproad.git
synced 2024-09-20 00:21:15 +01:00
Merge pull request #5 from gmemstr/refactoring
Huge refactoring, simplification and expansion.
This commit is contained in:
commit
8127aaa6a8
41
.circleci/config.yml
Normal file
41
.circleci/config.yml
Normal 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
7
.gitignore
vendored
|
@ -14,12 +14,13 @@
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
# Config file
|
# Config files
|
||||||
config.json
|
providers.yml
|
||||||
|
auth.yml
|
||||||
|
|
||||||
# Binary
|
# Binary
|
||||||
nas
|
nas
|
||||||
|
build/
|
||||||
|
|
||||||
*.db
|
*.db
|
||||||
.lock
|
.lock
|
||||||
assets/web/*
|
|
35
Makefile
Normal file
35
Makefile
Normal 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
|
84
README.md
84
README.md
|
@ -1,30 +1,76 @@
|
||||||
# nas
|
# sliproad
|
||||||
small go nas platform for my raspberry pi
|
merging filesystems together
|
||||||
|
|
||||||
## usage
|
## about
|
||||||
|
|
||||||
```
|
this project aims to be an easy-to-user web API that allows the management of cloud storage, whether it be on
|
||||||
cp assets/config/config.example.json assets/config/config.json
|
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
|
||||||
# edit config file with your hot/cold storage locations
|
i hope allows others to pick it up and improve on it down the line.
|
||||||
nano assets/config/config.json
|
|
||||||
# run
|
if something is unclear, feel free to open an issue :)
|
||||||
go run webserver.go
|
|
||||||
# or build and run
|
## configuration
|
||||||
go build; ./nas
|
|
||||||
|
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.
|
||||||
|
|
||||||
svg icons via https://iconsvg.xyz
|
## providers
|
||||||
|
|
||||||
raspberry pi svg via https://www.vectorlogo.zone/logos/raspberrypi/index.html
|
"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.
|
||||||
|
|
||||||
|
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)
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"ColdStorage": "",
|
|
||||||
"HotStorage": ""
|
|
||||||
}
|
|
0
assets/config_examples/README.md
Normal file
0
assets/config_examples/README.md
Normal file
3
assets/config_examples/auth.yml
Normal file
3
assets/config_examples/auth.yml
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
provider_url: "http://localhost:8080"
|
||||||
|
realm: "nas"
|
||||||
|
redirect_base_url: "http://localhost:3000"
|
9
assets/config_examples/backblaze.yml
Normal file
9
assets/config_examples/backblaze.yml
Normal 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
|
5
assets/config_examples/disk.yml
Normal file
5
assets/config_examples/disk.yml
Normal 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.
|
20
assets/config_examples/providers.yml
Normal file
20
assets/config_examples/providers.yml
Normal 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
203
assets/web/css/styles.css
Normal 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;
|
||||||
|
}
|
23
assets/web/icons/README.md
Normal file
23
assets/web/icons/README.md
Normal 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.
|
5
assets/web/icons/file.svg
Normal file
5
assets/web/icons/file.svg
Normal 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 |
4
assets/web/icons/folder.svg
Normal file
4
assets/web/icons/folder.svg
Normal 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 |
8
assets/web/icons/trash.svg
Normal file
8
assets/web/icons/trash.svg
Normal 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 |
|
@ -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>
|
159
assets/web/javascript/app.js
Normal file
159
assets/web/javascript/app.js
Normal 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)
|
||||||
|
}
|
277
auth/auth.go
277
auth/auth.go
|
@ -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))
|
|
||||||
}
|
|
28
authentication/keycloak.go
Normal file
28
authentication/keycloak.go
Normal 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
|
||||||
|
}
|
|
@ -1 +0,0 @@
|
||||||
env GOOS=linux GOARCH=arm GOARM=5 go build
|
|
|
@ -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
276
files/backblaze.go
Normal 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
103
files/disk.go
Normal 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
91
files/fileprovider.go
Normal 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
|
||||||
|
}
|
194
files/files.go
194
files/files.go
|
@ -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
117
files/files_test.go
Normal 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
36
files/fileutils.go
Normal 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
23
frontend/.gitignore
vendored
|
@ -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*
|
|
|
@ -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 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
|
|
13307
frontend/package-lock.json
generated
13307
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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 |
|
@ -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>
|
|
|
@ -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"
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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();
|
|
|
@ -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 |
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
10112
frontend/yarn.lock
10112
frontend/yarn.lock
File diff suppressed because it is too large
Load diff
12
go.mod
12
go.mod
|
@ -1,10 +1,12 @@
|
||||||
module gmem.ca/nas
|
module github.com/gmemstr/nas
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722
|
github.com/Nerzal/gocloak/v5 v5.1.0
|
||||||
github.com/gorilla/mux v1.7.4 // indirect
|
github.com/go-yaml/yaml v2.1.0+incompatible
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
github.com/gorilla/mux v1.7.4
|
||||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d
|
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
40
go.sum
|
@ -1,13 +1,37 @@
|
||||||
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722 h1:LmCeaQfQHTfKtx1HbR9cHMndomz14InE0d5lBWp/opI=
|
github.com/Nerzal/gocloak/v5 v5.1.0 h1:1YP4+GoY1DZ1k7WyNNr8xbFyt55B9ORn2ZHu+XqUK0Q=
|
||||||
github.com/gmemstr/nas v0.0.0-20190728044305-652472635722/go.mod h1:hv1O7aXobFKTuw2JKHkzEnFMdQqv6wcLEgoTD7mxIY4=
|
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 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
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/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
|
||||||
golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
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=
|
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
57
router/authrouter.go
Normal 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
147
router/filerouter.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
189
router/router.go
189
router/router.go
|
@ -1,189 +1,110 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gmemstr/nas/auth"
|
"io"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gmemstr/nas/common"
|
|
||||||
"github.com/gmemstr/nas/files"
|
|
||||||
"github.com/gmemstr/nas/system"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Handle(handlers ...common.Handler) http.Handler {
|
type handler func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
|
||||||
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 {
|
for _, handler := range handlers {
|
||||||
err := handler(rc, w, r)
|
err := handler(context, w, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("%v", err)
|
log.Printf("%v", err)
|
||||||
|
|
||||||
w.Write([]byte(http.StatusText(err.StatusCode)))
|
w.Write([]byte(http.StatusText(err.StatusCode)))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actual router, define endpoints here.
|
// Init initializes the main router and all routes for the application.
|
||||||
func Init() *mux.Router {
|
func Init() *mux.Router {
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
// "Static" paths
|
// "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
|
// Paths that require specific handlers
|
||||||
r.Handle("/", Handle(
|
r.Handle("/", handle(
|
||||||
auth.RequireAuthorization(1),
|
requiresAuth(),
|
||||||
rootHandler(),
|
rootHandler(),
|
||||||
)).Methods("GET")
|
)).Methods("GET")
|
||||||
|
|
||||||
r.Handle(`/login`, Handle(
|
// File & Provider API
|
||||||
loginHandler(),
|
r.Handle("/api/providers", handle(
|
||||||
)).Methods("POST", "GET")
|
requiresAuth(),
|
||||||
|
listProviders(),
|
||||||
r.Handle("/api/diskusage", Handle(
|
|
||||||
auth.RequireAuthorization(1),
|
|
||||||
system.DiskUsages(),
|
|
||||||
)).Methods("GET")
|
)).Methods("GET")
|
||||||
|
|
||||||
r.Handle(`/api/{tier:(?:hot|cold)}/file/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
|
r.Handle(`/api/files/{provider:[a-zA-Z0-9]+\/*}`, handle(
|
||||||
auth.RequireAuthorization(1),
|
requiresAuth(),
|
||||||
files.ViewFile(),
|
handleProvider(),
|
||||||
)).Methods("GET")
|
)).Methods("GET", "POST")
|
||||||
|
|
||||||
r.Handle("/api/upload/hot", Handle(
|
r.Handle(`/api/files/{provider:[a-zA-Z0-9]+}/{file:.+}`, handle(
|
||||||
auth.RequireAuthorization(1),
|
requiresAuth(),
|
||||||
files.UploadFile(),
|
handleProvider(),
|
||||||
)).Methods("POST")
|
)).Methods("GET", "POST", "DELETE")
|
||||||
|
|
||||||
r.Handle("/api/upload/{tier:(?:hot|cold)}", Handle(
|
// Auth API & Endpoints
|
||||||
auth.RequireAuthorization(1),
|
r.Handle(`/api/auth/callback`, handle(
|
||||||
files.UploadFile(),
|
callbackAuth(),
|
||||||
)).Methods("POST")
|
)).Methods("GET", "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")
|
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles serving index page.
|
||||||
func loginHandler() common.Handler {
|
func rootHandler() handler {
|
||||||
return func(rc *common.RouterContext, w http.ResponseWriter, r *http.Request) *common.HTTPError {
|
return func(context *requestContext, w http.ResponseWriter, r *http.Request) *httpError {
|
||||||
if r.Method == "GET" {
|
f, err := os.Open("assets/web/index.html")
|
||||||
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 {
|
if err != nil {
|
||||||
return &common.HTTPError{
|
return &httpError{
|
||||||
Message: fmt.Sprintf("error in reading user database: %v", err),
|
Message: fmt.Sprintf("error serving index page from assets/web"),
|
||||||
StatusCode: http.StatusInternalServerError,
|
StatusCode: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
statement, err := db.Prepare("SELECT * FROM users WHERE username=?")
|
defer f.Close()
|
||||||
|
stats, err := f.Stat()
|
||||||
if _, err := auth.DecryptCookie(r); err == nil {
|
|
||||||
http.Redirect(w, r, "/admin", http.StatusTemporaryRedirect)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.ParseForm()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &common.HTTPError{
|
return &httpError{
|
||||||
Message: fmt.Sprintf("error in parsing form: %v", err),
|
Message: fmt.Sprintf("error serving index page from assets/web"),
|
||||||
StatusCode: http.StatusBadRequest,
|
StatusCode: http.StatusInternalServerError,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
w.Header().Add("Content-Length", strconv.FormatInt(stats.Size(), 10))
|
||||||
}
|
}
|
||||||
|
|
||||||
username := r.Form.Get("username")
|
_, err = io.Copy(w, f)
|
||||||
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 {
|
if err != nil {
|
||||||
return &common.HTTPError{
|
return &httpError{
|
||||||
Message: fmt.Sprintf("error in decoding sql data", err),
|
Message: fmt.Sprintf("error serving index page from assets/web"),
|
||||||
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,
|
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 nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return &common.HTTPError{
|
|
||||||
Message: "Invalid credentials!",
|
|
||||||
StatusCode: http.StatusUnauthorized,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
103
webserver.go
103
webserver.go
|
@ -1,92 +1,45 @@
|
||||||
/* webserver.go
|
|
||||||
*
|
|
||||||
* This is the webserver handler for Pogo, and handles
|
|
||||||
* all incoming connections, including authentication.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
|
"github.com/gmemstr/nas/authentication"
|
||||||
|
"github.com/gmemstr/nas/files"
|
||||||
"github.com/gmemstr/nas/router"
|
"github.com/gmemstr/nas/router"
|
||||||
|
"github.com/go-yaml/yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main function that defines routes
|
// Main function that defines routes
|
||||||
func main() {
|
func main() {
|
||||||
if _, err := os.Stat(".lock"); os.IsNotExist(err) {
|
// Initialize file providers.
|
||||||
createDatabase()
|
file, err := ioutil.ReadFile("providers.yml")
|
||||||
createLockFile()
|
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()
|
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))
|
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
|
|
||||||
}
|
|
Loading…
Reference in a new issue