diff --git a/.gitignore b/.gitignore index 3f5d67b..7839093 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ actions gongs reveil +routines tracks vendor ui/build diff --git a/api/routines.go b/api/routines.go index 4be776e..16d7c0a 100644 --- a/api/routines.go +++ b/api/routines.go @@ -1,37 +1,81 @@ package api import ( + "fmt" "net/http" "github.com/gin-gonic/gin" "git.nemunai.re/nemunaire/reveil/config" + "git.nemunai.re/nemunaire/reveil/model" ) func declareRoutinesRoutes(cfg *config.Config, router *gin.RouterGroup) { router.GET("/routines", func(c *gin.Context) { + routines, err := reveil.LoadRoutines(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + c.JSON(http.StatusOK, routines) }) router.POST("/routines", func(c *gin.Context) { - + c.AbortWithStatusJSON(http.StatusNotImplemented, gin.H{"errmsg": "TODO"}) }) - routinesRoutes := router.Group("/routines/:gid") - routinesRoutes.Use(routineHandler) + routinesRoutes := router.Group("/routines/:tid") + routinesRoutes.Use(func(c *gin.Context) { + routines, err := reveil.LoadRoutines(cfg) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": err.Error()}) + return + } + + for _, t := range routines { + if t.Id.ToString() == c.Param("tid") { + c.Set("routine", t) + c.Next() + return + } + } + + c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"errmsg": "Routine not found"}) + }) routinesRoutes.GET("", func(c *gin.Context) { c.JSON(http.StatusOK, c.MustGet("routine")) }) routinesRoutes.PUT("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("routine")) + oldroutine := c.MustGet("routine").(*reveil.Routine) + + var routine reveil.Routine + if err := c.ShouldBindJSON(&routine); err != nil { + c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) + return + } + + if routine.Name != oldroutine.Name { + err := oldroutine.Rename(routine.Name) + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to rename the routine: %s", err.Error())}) + return + } + } + + // TODO: change actions + + c.JSON(http.StatusOK, oldroutine) }) routinesRoutes.DELETE("", func(c *gin.Context) { - c.JSON(http.StatusOK, c.MustGet("routine")) + routine := c.MustGet("routine").(*reveil.Routine) + + err := routine.Remove() + if err != nil { + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": fmt.Sprintf("Unable to remove the routine: %s", err.Error())}) + return + } + + c.JSON(http.StatusOK, nil) }) } - -func routineHandler(c *gin.Context) { - c.Set("routine", nil) - - c.Next() -} diff --git a/model/routine.go b/model/routine.go new file mode 100644 index 0000000..86b06a2 --- /dev/null +++ b/model/routine.go @@ -0,0 +1,119 @@ +package reveil + +import ( + "crypto/sha512" + "io/fs" + "io/ioutil" + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "git.nemunai.re/nemunaire/reveil/config" +) + +type RoutineStep struct { + Delay uint64 `json:"delay"` + Action string `json:"action"` + Args []string `json:"args,omitempty"` +} + +type Routine struct { + Id Identifier `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Steps []RoutineStep `json:"steps"` +} + +func LoadRoutine(path string, cfg *config.Config) ([]RoutineStep, error) { + ds, err := ioutil.ReadDir(path) + if err != nil { + return nil, err + } + + actionsDir, err := filepath.Abs(cfg.ActionsDir) + if err != nil { + return nil, err + } + + var steps []RoutineStep + + for _, f := range ds { + fullpath := filepath.Join(path, f.Name()) + if f.Mode()&os.ModeSymlink == 0 { + continue + } + + dst, err := os.Readlink(fullpath) + if err != nil { + return nil, err + } + + if adst, err := filepath.Abs(filepath.Join(path, dst)); err == nil { + dst = adst + } + + args := strings.Split(f.Name(), "_") + + delay, err := strconv.ParseUint(args[0], 10, 64) + + step := RoutineStep{ + Delay: delay, + Action: strings.TrimPrefix(dst, actionsDir+"/"), + } + if len(args) > 1 { + step.Args = args[1:] + } + + steps = append(steps, step) + } + + return steps, nil +} + +func LoadRoutines(cfg *config.Config) (routines []*Routine, err error) { + err = filepath.Walk(cfg.RoutinesDir, func(path string, d fs.FileInfo, err error) error { + if d.IsDir() && path != cfg.RoutinesDir { + hash := sha512.Sum512([]byte(path)) + + // Explore directory + steps, err := LoadRoutine(path, cfg) + if err != nil { + log.Printf("Invalid routine directory (trying to walk through %s): %s", path, err.Error()) + // Ignore invalid routines + return nil + } + + routines = append(routines, &Routine{ + Id: hash[:], + Name: d.Name(), + Path: path, + Steps: steps, + }) + } + + return nil + }) + + return +} + +func (r *Routine) Rename(newName string) error { + newPath := filepath.Join(filepath.Dir(r.Path), newName) + + err := os.Rename( + r.Path, + newPath, + ) + if err != nil { + return err + } + + r.Path = newPath + return nil +} + +func (a *Routine) Remove() error { + return os.Remove(a.Path) +} diff --git a/ui/src/components/CardRoutine.svelte b/ui/src/components/CardRoutine.svelte index 872ddf5..b0c252d 100644 --- a/ui/src/components/CardRoutine.svelte +++ b/ui/src/components/CardRoutine.svelte @@ -1,7 +1,9 @@ @@ -49,11 +37,26 @@ > - {routine.title} + {routine.name} - - {#each routine.steps as step (step.id)} - {step.name} - {/each} - + {#if routine.steps} + + {#each routine.steps as step} + + {#if $actions_idx && $actions_idx[step.action]} + {$actions_idx[step.action].name} + {:else} + {step.action} + {/if} + + {step.delay/60} min + + + {/each} + + {:else} + + Aucune action définie. + + {/if} diff --git a/ui/src/lib/routine.js b/ui/src/lib/routine.js new file mode 100644 index 0000000..31c8f72 --- /dev/null +++ b/ui/src/lib/routine.js @@ -0,0 +1,61 @@ +export class Routine { + constructor(res) { + if (res) { + this.update(res); + } + } + + update({ id, name, path, steps }) { + this.id = id; + this.name = name; + this.path = path; + + steps.sort((a, b) => a.delay - b.delay); + this.steps = steps; + } + + async delete() { + const res = await fetch(`api/routines/${this.id}`, { + method: 'DELETE', + headers: {'Accept': 'application/json'} + }); + if (res.status == 200) { + return true; + } else { + throw new Error((await res.json()).errmsg); + } + } + + async save() { + const res = await fetch(this.id?`api/routines/${this.id}`:'api/routines', { + method: this.id?'PUT':'POST', + headers: {'Accept': 'application/json'}, + body: JSON.stringify(this), + }); + if (res.status == 200) { + const data = await res.json(); + this.update(data); + return data; + } else { + throw new Error((await res.json()).errmsg); + } + } +} + +export async function getRoutines() { + const res = await fetch(`api/routines`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return (await res.json()).map((r) => new Routine(r)); + } else { + throw new Error((await res.json()).errmsg); + } +} + +export async function getRoutine(rid) { + const res = await fetch(`api/routines/${rid}`, {headers: {'Accept': 'application/json'}}) + if (res.status == 200) { + return new Routine(await res.json()); + } else { + throw new Error((await res.json()).errmsg); + } +} diff --git a/ui/src/routes/routines/+page.svelte b/ui/src/routes/routines/+page.svelte index 8678880..08ad7e5 100644 --- a/ui/src/routes/routines/+page.svelte +++ b/ui/src/routes/routines/+page.svelte @@ -3,10 +3,13 @@ Card, Col, Container, - Row, Icon, + Row, + Spinner, } from 'sveltestrap'; + import { routines } from '../../stores/routines'; + import CardRoutine from '../../components/CardRoutine.svelte'; import ActionList from '../../components/ActionList.svelte'; @@ -16,7 +19,19 @@ - + {#if $routines.list} + {#each $routines.list as routine (routine.id)} + + {/each} + {:else} + {#await routines.refresh()} +
+ Chargement en cours… +
+ {:then} + test + {/await} + {/if} { const list = await getActions(); - update((m) => Object.assign(m, {list})); + const fileIdx = {}; + list.forEach(function(action, k) { + fileIdx[action.path] = action; + }); + + update((m) => (Object.assign(m, {list, fileIdx}))); return list; }, + getActionByFilename: (fname) => { + + }, + update: (res_actions, cb=null) => { if (res_actions.status === 200) { res_actions.json().then((list) => { - update((m) => (Object.assign(m, {list}))); + const fileIdx = {}; + list.forEach(function(action, k) { + fileIdx[action.path] = action; + }) + + update((m) => (Object.assign(m, {list, fileIdx}))); if (cb) { cb(list); @@ -34,3 +48,8 @@ function createActionsStore() { } export const actions = createActionsStore(); + +export const actions_idx = derived( + actions, + ($actions) => ($actions.fileIdx), +); diff --git a/ui/src/stores/routines.js b/ui/src/stores/routines.js new file mode 100644 index 0000000..02ef9a0 --- /dev/null +++ b/ui/src/stores/routines.js @@ -0,0 +1,36 @@ +import { writable } from 'svelte/store'; + +import { getRoutines } from '../lib/routine' + +function createRoutinesStore() { + const { subscribe, set, update } = writable({list: null}); + + return { + subscribe, + + set: (v) => { + update((m) => Object.assign(m, v)); + }, + + refresh: async () => { + const list = await getRoutines(); + update((m) => Object.assign(m, {list})); + return list; + }, + + update: (res_routines, cb=null) => { + if (res_routines.status === 200) { + res_routines.json().then((list) => { + update((m) => (Object.assign(m, {list}))); + + if (cb) { + cb(list); + } + }); + } + }, + }; + +} + +export const routines = createRoutinesStore();