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}
+
+

{message}

+
+ + ) +} 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 ( + <> +
+ +
+
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") }