diff --git a/_frontend/index.html b/_frontend/index.html
index 060d8a5..a402098 100644
--- a/_frontend/index.html
+++ b/_frontend/index.html
@@ -4,8 +4,13 @@
+
Homepage
+
+
+
+
diff --git a/_frontend/src/components/MessageWidget.jsx b/_frontend/src/components/MessageWidget.jsx
new file mode 100644
index 0000000..3cc3464
--- /dev/null
+++ b/_frontend/src/components/MessageWidget.jsx
@@ -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 (
+ <>
+ {title}
+
+ >
+ )
+}
diff --git a/_frontend/src/components/PieChartWidget.jsx b/_frontend/src/components/PieChartWidget.jsx
new file mode 100644
index 0000000..223039e
--- /dev/null
+++ b/_frontend/src/components/PieChartWidget.jsx
@@ -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 (
+ <>
+ {title}
+
+
+
+ >
+ )
+}
diff --git a/_frontend/src/home.jsx b/_frontend/src/home.jsx
index 0411f3a..a10fa3b 100644
--- a/_frontend/src/home.jsx
+++ b/_frontend/src/home.jsx
@@ -1,9 +1,53 @@
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 = () => (
- <>
- Homepage
- >
-)
+const WidgetTypes = {
+ pie: PieChartWidget,
+ message: MessageWidget,
+}
+
+const Widget = ({ type, value }) => {
+ const CustomWidget = WidgetTypes[type]
+
+ return (
+
+
+
+ )
+}
+
+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 (
+ <>
+
+ Dashboard
+ •
+ space.phc.dm.unipi.it
+
+
+ (Viewing page as {user})
+
+
+ {widgets.map(w => (
+
+ ))}
+
+ >
+ )
+}
render(, document.querySelector('main'))
diff --git a/_frontend/src/util.js b/_frontend/src/util.js
new file mode 100644
index 0000000..c67e789
--- /dev/null
+++ b/_frontend/src/util.js
@@ -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
+}
diff --git a/_frontend/styles/main.scss b/_frontend/styles/main.scss
index dcbc2a0..0ef923c 100644
--- a/_frontend/styles/main.scss
+++ b/_frontend/styles/main.scss
@@ -4,19 +4,124 @@
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 {
margin: 0;
width: 100%;
min-height: 100vh;
- font-family: 'Inter', 'Segoe UI', 'Helvetica', 'Arial', sans-serif;
+ font-family: var(--ft-sans);
font-size: 16px;
+
+ color: var(--fg-500);
+ background: var(--bg-500);
+}
+
+//
+// Structure
+//
+
+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);
}
-// Headings
+p {
+ margin: 0;
+
+ & + p {
+ margin-top: 0.5rem;
+ }
+}
-$base-font-size: 18px;
+$base-font-size: 16px;
$heading-scale: 1.33;
@function pow($number, $exponent) {
diff --git a/config.go b/config/config.go
similarity index 62%
rename from config.go
rename to config/config.go
index 7b103f8..be63724 100644
--- a/config.go
+++ b/config/config.go
@@ -1,4 +1,4 @@
-package main
+package config
import (
"log"
@@ -7,13 +7,13 @@ import (
"github.com/joho/godotenv"
)
-var Config struct {
+var (
Mode string
Host string
BaseURL string
AdminPassword string
-}
+)
func loadEnv(key string, defaultValue ...string) string {
env := os.Getenv(key)
@@ -33,9 +33,9 @@ func init() {
// Load Config
godotenv.Load()
- Config.Mode = loadEnv(os.Getenv("MODE"), "development")
- Config.Host = loadEnv(os.Getenv("HOST"), ":4000")
- Config.BaseURL = loadEnv(os.Getenv("HOST"), "http://localhost:4000")
+ Mode = loadEnv(os.Getenv("MODE"), "development")
+ Host = loadEnv(os.Getenv("HOST"), ":4000")
+ BaseURL = loadEnv(os.Getenv("HOST"), "http://localhost:4000")
- Config.AdminPassword = loadEnv(os.Getenv("ADMIN_PASSWORD"), "secret")
+ AdminPassword = loadEnv(os.Getenv("ADMIN_PASSWORD"), "secret")
}
diff --git a/database/database.go b/database/database.go
index d1f9911..c9441dd 100644
--- a/database/database.go
+++ b/database/database.go
@@ -2,34 +2,71 @@ package database
import (
"encoding/json"
+ "fmt"
+ "io"
"log"
"os"
"git.phc.dm.unipi.it/phc/storage/store"
+ "golang.org/x/exp/slices"
)
+type MonitorWidget struct {
+ Type string `json:"type"`
+ Value any `json:"value"`
+}
+
+type DashboardState struct {
+ Widgets []MonitorWidget `json:"widgets"`
+}
+
type Database interface {
- Buckets() ([]string, error)
+ // 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
- Bucket(bucket string) (store.Store, error)
+ GetBucketObject(bucket, id string, w io.Writer) error
+ SetBucketObject(bucket, id string, r io.Reader) error
+
+ AllBucketObjects(bucket string) ([]string, error)
}
-type bucketInfo struct {
- Name string `json:"name"`
- Path string `json:"path"`
+type jsonBucket struct {
+ Name string `json:"name"`
+ Path string `json:"path"`
+ Objects []string `json:"objects"`
}
type jsonDB struct {
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 {
db := &jsonDB{
- file: file,
- BucketsInfo: map[string]bucketInfo{},
+ file: file,
+ PrefixSize: 2,
+ DashboardState: DashboardState{
+ Widgets: []MonitorWidget{},
+ },
+ Buckets: map[string]*jsonBucket{},
}
err := db.load()
@@ -96,37 +133,145 @@ func (db *jsonDB) load() error {
return nil
}
-func (db *jsonDB) Buckets() ([]string, error) {
+func (db *jsonDB) GetDashboardState() (DashboardState, error) {
db.load()
- buckets := make([]string, 0, len(db.BucketsInfo))
+ return db.DashboardState, nil
+}
- for _, b := range db.BucketsInfo {
- buckets = append(buckets, b.Name)
- }
+func (db *jsonDB) SetDashboardState(state DashboardState) error {
+ db.load()
+ db.DashboardState = state
- return buckets, nil
+ db.store()
+ return nil
}
func (db *jsonDB) CreateBucket(bucket string, options ...any) error {
db.load()
- defer db.store()
- bi := bucketInfo{Name: bucket, Path: bucket + "/"}
+ 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.BucketsInfo[bucket] = bi
+ 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) Bucket(bucket string) (store.Store, error) {
- b := db.BucketsInfo[bucket]
+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)
+ }
+
+ return buckets, nil
+}
+
+func (db *jsonDB) CreateBucketObject(bucket string, r io.Reader) (string, error) {
+ db.load()
+
+ b, found := db.Buckets[bucket]
+ if !found {
+ return "", fmt.Errorf("bucket named %q not found", bucket)
+ }
+
+ 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
+}
+
+func (db *jsonDB) AllBucketObjects(bucket string) ([]string, error) {
+ db.load()
+
+ b, found := db.Buckets[bucket]
+ if !found {
+ return nil, fmt.Errorf("bucket named %q not found", bucket)
+ }
+
+ objects, err := store.All(b.Path)
+ if err != nil {
+ return nil, err
+ }
+
+ b.Objects = objects
- return &store.DirStore{
- BaseDir: b.Path,
- Prefix: 2,
- }, nil
+ db.store()
+ return b.Objects, nil
}
diff --git a/go.mod b/go.mod
index a7ba831..7a5acb5 100644
--- a/go.mod
+++ b/go.mod
@@ -13,5 +13,6 @@ require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.37.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
)
diff --git a/go.sum b/go.sum
index e1727ab..c545c93 100644
--- a/go.sum
+++ b/go.sum
@@ -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/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
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-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/main.go b/main.go
index 8e535fe..cfc2b31 100644
--- a/main.go
+++ b/main.go
@@ -6,6 +6,7 @@ import (
"os/exec"
"strings"
+ "git.phc.dm.unipi.it/phc/storage/config"
"git.phc.dm.unipi.it/phc/storage/database"
"git.phc.dm.unipi.it/phc/storage/routes"
@@ -30,7 +31,7 @@ func main() {
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"`)
cmd := exec.Command("sh", "-c", "cd _frontend/ && npm run dev")
cmdStdout, _ := cmd.StdoutPipe()
@@ -51,5 +52,5 @@ func main() {
}
}
- log.Fatal(app.Listen(Config.Host))
+ log.Fatal(app.Listen(config.Host))
}
diff --git a/routes/api.go b/routes/api.go
index 3872b3c..d2edf9a 100644
--- a/routes/api.go
+++ b/routes/api.go
@@ -2,18 +2,84 @@ package routes
import (
"bytes"
+ "fmt"
"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"
)
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 {
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 {
- buckets, err := r.Database.Buckets()
+ buckets, err := r.Database.AllBuckets()
if err != nil {
return err
}
@@ -45,14 +111,20 @@ func (r *Router) Api(api fiber.Router) {
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")
- b, err := r.Database.Bucket(bucket)
+ objects, err := r.Database.AllBucketObjects(bucket)
if err != nil {
return err
}
+ return c.JSON(objects)
+ })
+
+ api.Post("/buckets/:bucket", func(c *fiber.Ctx) error {
+ bucket := c.Params("bucket")
+
ff, err := c.FormFile("file")
if err != nil {
return err
@@ -63,7 +135,7 @@ func (r *Router) Api(api fiber.Router) {
return err
}
- id, err := b.Create(mf)
+ id, err := r.Database.CreateBucketObject(bucket, mf)
if err != nil {
return err
}
@@ -80,12 +152,7 @@ func (r *Router) Api(api fiber.Router) {
buf := &bytes.Buffer{}
- b, err := r.Database.Bucket(bucket)
- if err != nil {
- return err
- }
-
- if err := b.Read(id, buf); err != nil {
+ if err := r.Database.GetBucketObject(bucket, id, buf); err != nil {
return err
}
diff --git a/store/store.go b/store/store.go
index 3fb44f7..a179acc 100644
--- a/store/store.go
+++ b/store/store.go
@@ -9,136 +9,79 @@ import (
"git.phc.dm.unipi.it/phc/storage/utils"
)
-type Store interface {
- // Create a new object by reading from the given io.Reader and returns its new id
- Create(r io.Reader) (string, error)
+const RandomIdSize = 32
- // Read the object identified by "id" into the given io.Writer
- Read(id string, w io.Writer) error
-
- // 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 split(baseDir string, prefixSize int, id string) (prefix, rest string) {
+ return id[:prefixSize], id[prefixSize:]
}
-//
-// In Memory Byte Store
-//
+func Create(baseDir string, prefixSize int, r io.Reader) (string, error) {
+ id := utils.GenerateRandomString(RandomIdSize)
+ prefix, rest := split(baseDir, prefixSize, id)
-type memStore struct {
- objects map[string][]byte
-}
+ os.MkdirAll(path.Join(baseDir, prefix), os.ModePerm)
-func NewMemStore() Store {
- return &memStore{
- objects: map[string][]byte{},
+ f, err := os.Create(path.Join(baseDir, prefix, rest))
+ if err != nil {
+ return "", err
}
-}
-// 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 {
+ if _, err := io.Copy(f, r); 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)
- }
+func Read(baseDir string, prefixSize int, id string, w io.Writer) error {
+ prefix, rest := split(baseDir, prefixSize, 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)
+ f, err := os.Open(path.Join(baseDir, prefix, rest))
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)
+ if _, err := io.Copy(w, f); err != nil {
+ return err
}
- 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))
+func All(baseDir string) ([]string, error) {
+ entries, err := os.ReadDir(baseDir)
if err != nil {
- return "", err
+ return nil, err
}
- if _, err := io.Copy(f, r); err != nil {
- return "", err
- }
+ objects := []string{}
- return id, nil
-}
+ for _, e := range entries {
+ if !e.IsDir() {
+ return nil, fmt.Errorf(`invalid store structure`)
+ }
-func (d *DirStore) Read(id string, w io.Writer) error {
- prefix, rest := d.split(id)
+ objectsEntries, err := os.ReadDir(path.Join(baseDir, e.Name()))
+ if err != nil {
+ return nil, err
+ }
- f, err := os.Open(path.Join(d.BaseDir, prefix, rest))
- if err != nil {
- return err
- }
+ for _, obj := range objectsEntries {
+ if obj.IsDir() {
+ return nil, fmt.Errorf(`invalid store structure`)
+ }
- if _, err := io.Copy(w, f); err != nil {
- return err
+ objects = append(objects, e.Name()+obj.Name())
+ }
}
- return nil
+ return objects, nil
}
-func (d *DirStore) Update(id string, r io.Reader) error {
+func Update(baseDir string, prefixSize int, id string, r io.Reader) error {
panic("TODO: Not implemented")
}
-func (d *DirStore) Delete(id string) error {
+func Delete(baseDir string, prefixSize int, id string) error {
panic("TODO: Not implemented")
}