Refactoring and dashboard state in DB
This commit is contained in:
@@ -4,8 +4,13 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
<title>Homepage</title>
|
<title>Homepage</title>
|
||||||
|
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
<link rel="stylesheet" href="/styles/main.scss">
|
<link rel="stylesheet" href="/styles/main.scss">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export const MessageWidget = ({ title, message }) => {
|
||||||
|
title = title || 'Testo'
|
||||||
|
message = message || 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sed, possimus.'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{message}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect, useRef } from 'preact/hooks'
|
||||||
|
import { hashCode } from '../util.js'
|
||||||
|
|
||||||
|
const CANVAS_SIZE = 350
|
||||||
|
|
||||||
|
export const PieChartWidget = ({ title, parts, labels, total }) => {
|
||||||
|
title = title || 'Grafico a torta'
|
||||||
|
|
||||||
|
parts = parts || [1]
|
||||||
|
labels = labels || []
|
||||||
|
|
||||||
|
const canvasRef = useRef()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canvasRef.current) {
|
||||||
|
const $canvas = canvasRef.current
|
||||||
|
|
||||||
|
$canvas.style.width = `${CANVAS_SIZE}px`
|
||||||
|
$canvas.style.height = `${CANVAS_SIZE}px`
|
||||||
|
|
||||||
|
const width = $canvas.width / 2
|
||||||
|
const height = $canvas.height / 2
|
||||||
|
|
||||||
|
const g = $canvas.getContext('2d')
|
||||||
|
g.resetTransform()
|
||||||
|
g.clearRect(0, 0, width * 2, height * 2)
|
||||||
|
|
||||||
|
g.scale(2, 2)
|
||||||
|
g.translate(width / 2, height / 2)
|
||||||
|
|
||||||
|
g.font = `18px 'Open Sans'`
|
||||||
|
g.textAlign = 'center'
|
||||||
|
g.textBaseline = 'middle'
|
||||||
|
|
||||||
|
total = total || parts.reduce((acc, p) => acc + p)
|
||||||
|
const anglesAndLabel = parts.map((p, i) => [(p / total) * 2 * Math.PI, labels[i] || ''])
|
||||||
|
|
||||||
|
g.fillStyle = `#ededed`
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, width * 0.5 * 0.8, width * 0.5 * 0.8, 0, 0, 2 * Math.PI)
|
||||||
|
g.fill()
|
||||||
|
|
||||||
|
g.strokeStyle = `#00000044`
|
||||||
|
g.beginPath()
|
||||||
|
g.ellipse(0, 0, width * 0.5 * 0.8, width * 0.5 * 0.8, 0, 0, 2 * Math.PI)
|
||||||
|
g.stroke()
|
||||||
|
|
||||||
|
let acc = 0
|
||||||
|
for (const [angle, label] of anglesAndLabel) {
|
||||||
|
g.fillStyle = `hsl(${((hashCode(label) % 0xff) * 360) / 0xff}, 80%, 65%)`
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.arc(0, 0, width * 0.5 * 0.8, acc - 0.5 * Math.PI, acc + angle - 0.5 * Math.PI)
|
||||||
|
g.fill()
|
||||||
|
|
||||||
|
g.strokeStyle = `#00000044`
|
||||||
|
g.beginPath()
|
||||||
|
g.moveTo(0, 0)
|
||||||
|
g.arc(0, 0, width * 0.5 * 0.8, acc - 0.5 * Math.PI, acc + angle - 0.5 * Math.PI)
|
||||||
|
g.stroke()
|
||||||
|
|
||||||
|
g.fillStyle = '#333'
|
||||||
|
g.fillText(
|
||||||
|
label,
|
||||||
|
Math.cos(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9,
|
||||||
|
Math.sin(acc + angle / 2 - 0.5 * Math.PI) * width * 0.5 * 0.9
|
||||||
|
)
|
||||||
|
|
||||||
|
acc += angle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [canvasRef, parts])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div class="title">{title}</div>
|
||||||
|
<div class="content">
|
||||||
|
<canvas ref={canvasRef} width={CANVAS_SIZE * 2} height={CANVAS_SIZE * 2}></canvas>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
+49
-5
@@ -1,9 +1,53 @@
|
|||||||
import { render } from 'preact'
|
import { render } from 'preact'
|
||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
import { MessageWidget } from './components/MessageWidget.jsx'
|
||||||
|
import { PieChartWidget } from './components/PieChartWidget.jsx'
|
||||||
|
import { useUser } from './util.js'
|
||||||
|
|
||||||
const App = () => (
|
const WidgetTypes = {
|
||||||
<>
|
pie: PieChartWidget,
|
||||||
<h1>Homepage</h1>
|
message: MessageWidget,
|
||||||
</>
|
}
|
||||||
)
|
|
||||||
|
const Widget = ({ type, value }) => {
|
||||||
|
const CustomWidget = WidgetTypes[type]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={'widget ' + type}>
|
||||||
|
<CustomWidget {...value} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const [widgets, setWidgets] = useState([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/dashboard-state')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(state => setWidgets(state.widgets))
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header>
|
||||||
|
<div class="logo">Dashboard</div>
|
||||||
|
<div class="spacer">•</div>
|
||||||
|
<div class="machine">space.phc.dm.unipi.it</div>
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
(Viewing page as <b>{user}</b>)
|
||||||
|
</p>
|
||||||
|
<div class="widgets">
|
||||||
|
{widgets.map(w => (
|
||||||
|
<Widget {...w} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
render(<App />, document.querySelector('main'))
|
render(<App />, document.querySelector('main'))
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useEffect, useState } from 'preact/hooks'
|
||||||
|
|
||||||
|
export function hashCode(s) {
|
||||||
|
s = s.toString() + "seed iniziale dell'hash"
|
||||||
|
|
||||||
|
let hash = 0
|
||||||
|
|
||||||
|
if (s.length === 0) return hash
|
||||||
|
|
||||||
|
for (let i = 0; i < s.length; i++) {
|
||||||
|
const chr = s.charCodeAt(i)
|
||||||
|
hash = (hash << 5) - hash + chr
|
||||||
|
hash |= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUser() {
|
||||||
|
const [user, setUser] = useState(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/current-user')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(value => setUser(value))
|
||||||
|
.catch(e => console.error(e))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
+108
-3
@@ -4,19 +4,124 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-100: #ffffff;
|
||||||
|
--bg-500: #f0f0f0;
|
||||||
|
--bg-600: #e0e0e0;
|
||||||
|
|
||||||
|
--fg-400: #3d3d3d;
|
||||||
|
--fg-500: #333;
|
||||||
|
|
||||||
|
--ft-sans: 'Open Sans', sans-serif;
|
||||||
|
|
||||||
|
--ft-sans-wt-light: 300;
|
||||||
|
--ft-sans-wt-normal: 400;
|
||||||
|
--ft-sans-wt-bold: 600;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|
||||||
font-family: 'Inter', 'Segoe UI', 'Helvetica', 'Arial', sans-serif;
|
font-family: var(--ft-sans);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
|
color: var(--fg-500);
|
||||||
|
background: var(--bg-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Headings
|
//
|
||||||
|
// Structure
|
||||||
|
//
|
||||||
|
|
||||||
$base-font-size: 18px;
|
main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
header {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
background: var(--bg-100);
|
||||||
|
border-bottom: 1px solid var(--bg-600);
|
||||||
|
|
||||||
|
color: var(--fg-400);
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: var(--ft-sans-wt-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: var(--ft-sans-wt-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.widgets {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
|
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
max-width: 120ch;
|
||||||
|
|
||||||
|
.widget {
|
||||||
|
background: var(--bg-100);
|
||||||
|
border: 1px solid var(--bg-600);
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
min-height: 10rem;
|
||||||
|
max-width: 50ch;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: var(--ft-sans-wt-bold);
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.todo {
|
||||||
|
width: 300px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Typography
|
||||||
|
//
|
||||||
|
|
||||||
|
b {
|
||||||
|
font-weight: var(--ft-sans-wt-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
& + p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$base-font-size: 16px;
|
||||||
$heading-scale: 1.33;
|
$heading-scale: 1.33;
|
||||||
|
|
||||||
@function pow($number, $exponent) {
|
@function pow($number, $exponent) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
@@ -7,13 +7,13 @@ import (
|
|||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Config struct {
|
var (
|
||||||
Mode string
|
Mode string
|
||||||
Host string
|
Host string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
|
||||||
AdminPassword string
|
AdminPassword string
|
||||||
}
|
)
|
||||||
|
|
||||||
func loadEnv(key string, defaultValue ...string) string {
|
func loadEnv(key string, defaultValue ...string) string {
|
||||||
env := os.Getenv(key)
|
env := os.Getenv(key)
|
||||||
@@ -33,9 +33,9 @@ func init() {
|
|||||||
// Load Config
|
// Load Config
|
||||||
godotenv.Load()
|
godotenv.Load()
|
||||||
|
|
||||||
Config.Mode = loadEnv(os.Getenv("MODE"), "development")
|
Mode = loadEnv(os.Getenv("MODE"), "development")
|
||||||
Config.Host = loadEnv(os.Getenv("HOST"), ":4000")
|
Host = loadEnv(os.Getenv("HOST"), ":4000")
|
||||||
Config.BaseURL = loadEnv(os.Getenv("HOST"), "http://localhost:4000")
|
BaseURL = loadEnv(os.Getenv("HOST"), "http://localhost:4000")
|
||||||
|
|
||||||
Config.AdminPassword = loadEnv(os.Getenv("ADMIN_PASSWORD"), "secret")
|
AdminPassword = loadEnv(os.Getenv("ADMIN_PASSWORD"), "secret")
|
||||||
}
|
}
|
||||||
+171
-26
@@ -2,34 +2,71 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.phc.dm.unipi.it/phc/storage/store"
|
"git.phc.dm.unipi.it/phc/storage/store"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Database interface {
|
type MonitorWidget struct {
|
||||||
Buckets() ([]string, error)
|
Type string `json:"type"`
|
||||||
CreateBucket(bucket string, options ...any) error
|
Value any `json:"value"`
|
||||||
|
|
||||||
Bucket(bucket string) (store.Store, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type bucketInfo struct {
|
type DashboardState struct {
|
||||||
Name string `json:"name"`
|
Widgets []MonitorWidget `json:"widgets"`
|
||||||
Path string `json:"path"`
|
}
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
// Dashboard DashboardState
|
||||||
|
|
||||||
|
GetDashboardState() (DashboardState, error)
|
||||||
|
SetDashboardState(DashboardState) error
|
||||||
|
|
||||||
|
// Bucket Operations
|
||||||
|
|
||||||
|
CreateBucket(bucket string, options ...any) error
|
||||||
|
DeleteBucket(bucket string) error
|
||||||
|
|
||||||
|
AllBuckets() ([]string, error)
|
||||||
|
|
||||||
|
// Bucket Object Operations
|
||||||
|
|
||||||
|
CreateBucketObject(bucket string, r io.Reader) (string, error)
|
||||||
|
DeleteBucketObject(bucket, id string) error
|
||||||
|
|
||||||
|
GetBucketObject(bucket, id string, w io.Writer) error
|
||||||
|
SetBucketObject(bucket, id string, r io.Reader) error
|
||||||
|
|
||||||
|
AllBucketObjects(bucket string) ([]string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonBucket struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Objects []string `json:"objects"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type jsonDB struct {
|
type jsonDB struct {
|
||||||
file string
|
file string
|
||||||
|
|
||||||
BucketsInfo map[string]bucketInfo `json:"buckets"`
|
PrefixSize int `json:"prefixSize"`
|
||||||
|
|
||||||
|
DashboardState DashboardState `json:"dashboardState"`
|
||||||
|
Buckets map[string]*jsonBucket `json:"buckets"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewJSON(file string) Database {
|
func NewJSON(file string) Database {
|
||||||
db := &jsonDB{
|
db := &jsonDB{
|
||||||
file: file,
|
file: file,
|
||||||
BucketsInfo: map[string]bucketInfo{},
|
PrefixSize: 2,
|
||||||
|
DashboardState: DashboardState{
|
||||||
|
Widgets: []MonitorWidget{},
|
||||||
|
},
|
||||||
|
Buckets: map[string]*jsonBucket{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := db.load()
|
err := db.load()
|
||||||
@@ -96,37 +133,145 @@ func (db *jsonDB) load() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *jsonDB) Buckets() ([]string, error) {
|
func (db *jsonDB) GetDashboardState() (DashboardState, error) {
|
||||||
db.load()
|
db.load()
|
||||||
|
|
||||||
buckets := make([]string, 0, len(db.BucketsInfo))
|
return db.DashboardState, nil
|
||||||
|
}
|
||||||
|
|
||||||
for _, b := range db.BucketsInfo {
|
func (db *jsonDB) SetDashboardState(state DashboardState) error {
|
||||||
|
db.load()
|
||||||
|
db.DashboardState = state
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) CreateBucket(bucket string, options ...any) error {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
if _, found := db.Buckets[bucket]; found {
|
||||||
|
return fmt.Errorf("bucket named %q already present", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
bi := &jsonBucket{
|
||||||
|
Name: bucket,
|
||||||
|
Path: bucket + "/",
|
||||||
|
Objects: []string{},
|
||||||
|
}
|
||||||
|
if len(options) > 0 {
|
||||||
|
bi.Path = options[0].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Buckets[bucket] = bi
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) DeleteBucket(bucket string) error {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
if _, found := db.Buckets[bucket]; !found {
|
||||||
|
return fmt.Errorf("bucket named %q not found", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(db.Buckets, bucket)
|
||||||
|
|
||||||
|
log.Printf("The bucket named %q was removed but its files are still on disk", bucket)
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) AllBuckets() ([]string, error) {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
buckets := make([]string, 0, len(db.Buckets))
|
||||||
|
|
||||||
|
for _, b := range db.Buckets {
|
||||||
buckets = append(buckets, b.Name)
|
buckets = append(buckets, b.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return buckets, nil
|
return buckets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *jsonDB) CreateBucket(bucket string, options ...any) error {
|
func (db *jsonDB) CreateBucketObject(bucket string, r io.Reader) (string, error) {
|
||||||
db.load()
|
db.load()
|
||||||
defer db.store()
|
|
||||||
|
|
||||||
bi := bucketInfo{Name: bucket, Path: bucket + "/"}
|
b, found := db.Buckets[bucket]
|
||||||
if len(options) > 0 {
|
if !found {
|
||||||
bi.Path = options[0].(string)
|
return "", fmt.Errorf("bucket named %q not found", bucket)
|
||||||
}
|
}
|
||||||
|
|
||||||
db.BucketsInfo[bucket] = bi
|
id, err := store.Create(b.Path, db.PrefixSize, r)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Objects = append(b.Objects, id)
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) SetBucketObject(bucket, id string, r io.Reader) error {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
b, found := db.Buckets[bucket]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("bucket named %q not found", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return store.Update(b.Path, db.PrefixSize, id, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) GetBucketObject(bucket, id string, w io.Writer) error {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
b, found := db.Buckets[bucket]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("bucket named %q not found", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.Read(b.Path, db.PrefixSize, id, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *jsonDB) DeleteBucketObject(bucket, id string) error {
|
||||||
|
db.load()
|
||||||
|
|
||||||
|
b, found := db.Buckets[bucket]
|
||||||
|
if !found {
|
||||||
|
return fmt.Errorf("bucket named %q not found", bucket)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Delete(b.Path, db.PrefixSize, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i := slices.Index(b.Objects, id)
|
||||||
|
b.Objects = append(b.Objects[:i], b.Objects[i+1:]...)
|
||||||
|
|
||||||
|
db.store()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *jsonDB) Bucket(bucket string) (store.Store, error) {
|
func (db *jsonDB) AllBucketObjects(bucket string) ([]string, error) {
|
||||||
b := db.BucketsInfo[bucket]
|
db.load()
|
||||||
|
|
||||||
return &store.DirStore{
|
b, found := db.Buckets[bucket]
|
||||||
BaseDir: b.Path,
|
if !found {
|
||||||
Prefix: 2,
|
return nil, fmt.Errorf("bucket named %q not found", bucket)
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
objects, err := store.All(b.Path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Objects = objects
|
||||||
|
|
||||||
|
db.store()
|
||||||
|
return b.Objects, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.37.0 // indirect
|
github.com/valyala/fasthttp v1.37.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
|
||||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ github.com/valyala/fasthttp v1.37.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxn
|
|||||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
|
||||||
|
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/storage/config"
|
||||||
"git.phc.dm.unipi.it/phc/storage/database"
|
"git.phc.dm.unipi.it/phc/storage/database"
|
||||||
"git.phc.dm.unipi.it/phc/storage/routes"
|
"git.phc.dm.unipi.it/phc/storage/routes"
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ func main() {
|
|||||||
|
|
||||||
app.Route("/api", router.Api)
|
app.Route("/api", router.Api)
|
||||||
|
|
||||||
if strings.HasPrefix(Config.Mode, "dev") {
|
if strings.HasPrefix(config.Mode, "dev") {
|
||||||
log.Printf(`Running dev server for frontend: "npm run dev"`)
|
log.Printf(`Running dev server for frontend: "npm run dev"`)
|
||||||
cmd := exec.Command("sh", "-c", "cd _frontend/ && npm run dev")
|
cmd := exec.Command("sh", "-c", "cd _frontend/ && npm run dev")
|
||||||
cmdStdout, _ := cmd.StdoutPipe()
|
cmdStdout, _ := cmd.StdoutPipe()
|
||||||
@@ -51,5 +52,5 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Fatal(app.Listen(Config.Host))
|
log.Fatal(app.Listen(config.Host))
|
||||||
}
|
}
|
||||||
|
|||||||
+77
-10
@@ -2,18 +2,84 @@ package routes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.phc.dm.unipi.it/phc/storage/config"
|
||||||
|
"git.phc.dm.unipi.it/phc/storage/database"
|
||||||
|
"git.phc.dm.unipi.it/phc/storage/utils"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *Router) Api(api fiber.Router) {
|
func (r *Router) Api(api fiber.Router) {
|
||||||
|
sessions := map[string]struct{}{}
|
||||||
|
|
||||||
|
api.Post("/login", func(c *fiber.Ctx) error {
|
||||||
|
var form struct {
|
||||||
|
Password string `form:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.BodyParser(&form); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Password == config.AdminPassword {
|
||||||
|
token := utils.GenerateRandomString(32)
|
||||||
|
sessions[token] = struct{}{}
|
||||||
|
|
||||||
|
c.Cookie(&fiber.Cookie{
|
||||||
|
Name: "sid",
|
||||||
|
Value: token,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().Add(3 * 24 * time.Hour),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON("ok")
|
||||||
|
})
|
||||||
|
|
||||||
api.Get("/status", func(c *fiber.Ctx) error {
|
api.Get("/status", func(c *fiber.Ctx) error {
|
||||||
return c.JSON("ok")
|
return c.JSON("ok")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.Get("/current-user", func(c *fiber.Ctx) error {
|
||||||
|
if _, found := sessions[c.Cookies("sid")]; !found {
|
||||||
|
return c.JSON("anonymous")
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON("admin")
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Get("/dashboard-state", func(c *fiber.Ctx) error {
|
||||||
|
state, err := r.Database.GetDashboardState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(state)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Post("/dashboard-state", func(c *fiber.Ctx) error {
|
||||||
|
if _, found := sessions[c.Cookies("sid")]; !found {
|
||||||
|
return fmt.Errorf("invalid session token")
|
||||||
|
}
|
||||||
|
|
||||||
|
var state database.DashboardState
|
||||||
|
|
||||||
|
if err := c.BodyParser(&state); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.Database.SetDashboardState(state); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON("ok")
|
||||||
|
})
|
||||||
|
|
||||||
api.Get("/buckets", func(c *fiber.Ctx) error {
|
api.Get("/buckets", func(c *fiber.Ctx) error {
|
||||||
buckets, err := r.Database.Buckets()
|
buckets, err := r.Database.AllBuckets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -45,14 +111,20 @@ func (r *Router) Api(api fiber.Router) {
|
|||||||
return c.JSON("ok")
|
return c.JSON("ok")
|
||||||
})
|
})
|
||||||
|
|
||||||
api.Post("/buckets/:bucket", func(c *fiber.Ctx) error {
|
api.Get("/buckets/:bucket", func(c *fiber.Ctx) error {
|
||||||
bucket := c.Params("bucket")
|
bucket := c.Params("bucket")
|
||||||
|
|
||||||
b, err := r.Database.Bucket(bucket)
|
objects, err := r.Database.AllBucketObjects(bucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return c.JSON(objects)
|
||||||
|
})
|
||||||
|
|
||||||
|
api.Post("/buckets/:bucket", func(c *fiber.Ctx) error {
|
||||||
|
bucket := c.Params("bucket")
|
||||||
|
|
||||||
ff, err := c.FormFile("file")
|
ff, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -63,7 +135,7 @@ func (r *Router) Api(api fiber.Router) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := b.Create(mf)
|
id, err := r.Database.CreateBucketObject(bucket, mf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -80,12 +152,7 @@ func (r *Router) Api(api fiber.Router) {
|
|||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
|
|
||||||
b, err := r.Database.Bucket(bucket)
|
if err := r.Database.GetBucketObject(bucket, id, buf); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b.Read(id, buf); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-100
@@ -9,106 +9,19 @@ import (
|
|||||||
"git.phc.dm.unipi.it/phc/storage/utils"
|
"git.phc.dm.unipi.it/phc/storage/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Store interface {
|
const RandomIdSize = 32
|
||||||
// Create a new object by reading from the given io.Reader and returns its new id
|
|
||||||
Create(r io.Reader) (string, error)
|
|
||||||
|
|
||||||
// Read the object identified by "id" into the given io.Writer
|
func split(baseDir string, prefixSize int, id string) (prefix, rest string) {
|
||||||
Read(id string, w io.Writer) error
|
return id[:prefixSize], id[prefixSize:]
|
||||||
|
|
||||||
// Update the object identified by "id" by reading from the given io.Reader
|
|
||||||
Update(id string, r io.Reader) error
|
|
||||||
|
|
||||||
// Delete the object with the given id
|
|
||||||
Delete(id string) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
func Create(baseDir string, prefixSize int, r io.Reader) (string, error) {
|
||||||
// In Memory Byte Store
|
id := utils.GenerateRandomString(RandomIdSize)
|
||||||
//
|
prefix, rest := split(baseDir, prefixSize, id)
|
||||||
|
|
||||||
type memStore struct {
|
os.MkdirAll(path.Join(baseDir, prefix), os.ModePerm)
|
||||||
objects map[string][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMemStore() Store {
|
f, err := os.Create(path.Join(baseDir, prefix, rest))
|
||||||
return &memStore{
|
|
||||||
objects: map[string][]byte{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new object by reading from the given io.Reader and returns its new id
|
|
||||||
func (m *memStore) Create(r io.Reader) (string, error) {
|
|
||||||
id := utils.GenerateRandomString(16)
|
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.objects[id] = data
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the object identified by "id" into the given io.Writer
|
|
||||||
func (m *memStore) Read(id string, w io.Writer) error {
|
|
||||||
data, found := m.objects[id]
|
|
||||||
if !found {
|
|
||||||
return fmt.Errorf("object with id %q not found", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the object identified by "id" by reading from the given io.Reader
|
|
||||||
func (m *memStore) Update(id string, r io.Reader) error {
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
m.objects[id] = data
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the object with the given id
|
|
||||||
func (m *memStore) Delete(id string) error {
|
|
||||||
if _, found := m.objects[id]; !found {
|
|
||||||
return fmt.Errorf("object with id %q not found", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(m.objects, id)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Dir Store
|
|
||||||
//
|
|
||||||
|
|
||||||
type DirStore struct {
|
|
||||||
// BaseDir is the root folder of this Store
|
|
||||||
BaseDir string
|
|
||||||
|
|
||||||
// Prefix is the number of letters to use for the first layer of folders
|
|
||||||
Prefix int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DirStore) split(id string) (prefix, rest string) {
|
|
||||||
return id[:d.Prefix], id[d.Prefix:]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DirStore) Create(r io.Reader) (string, error) {
|
|
||||||
id := utils.GenerateRandomString(16)
|
|
||||||
prefix, rest := d.split(id)
|
|
||||||
|
|
||||||
os.MkdirAll(path.Join(d.BaseDir, prefix), os.ModePerm)
|
|
||||||
|
|
||||||
f, err := os.Create(path.Join(d.BaseDir, prefix, rest))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -120,10 +33,10 @@ func (d *DirStore) Create(r io.Reader) (string, error) {
|
|||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DirStore) Read(id string, w io.Writer) error {
|
func Read(baseDir string, prefixSize int, id string, w io.Writer) error {
|
||||||
prefix, rest := d.split(id)
|
prefix, rest := split(baseDir, prefixSize, id)
|
||||||
|
|
||||||
f, err := os.Open(path.Join(d.BaseDir, prefix, rest))
|
f, err := os.Open(path.Join(baseDir, prefix, rest))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -135,10 +48,40 @@ func (d *DirStore) Read(id string, w io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DirStore) Update(id string, r io.Reader) error {
|
func All(baseDir string) ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
objects := []string{}
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if !e.IsDir() {
|
||||||
|
return nil, fmt.Errorf(`invalid store structure`)
|
||||||
|
}
|
||||||
|
|
||||||
|
objectsEntries, err := os.ReadDir(path.Join(baseDir, e.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, obj := range objectsEntries {
|
||||||
|
if obj.IsDir() {
|
||||||
|
return nil, fmt.Errorf(`invalid store structure`)
|
||||||
|
}
|
||||||
|
|
||||||
|
objects = append(objects, e.Name()+obj.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return objects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Update(baseDir string, prefixSize int, id string, r io.Reader) error {
|
||||||
panic("TODO: Not implemented")
|
panic("TODO: Not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DirStore) Delete(id string) error {
|
func Delete(baseDir string, prefixSize int, id string) error {
|
||||||
panic("TODO: Not implemented")
|
panic("TODO: Not implemented")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user