PJDTNLZMD5A5CDNA73435G72AJLAATL3OIFWZFCYLDSICZ4HGL7AC
package tags
import (
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/components"
"webster/MemoryLane/web/exceptions"
g "github.com/christophersw/gomponents-htmx"
"github.com/rs/zerolog/log"
)
func UpdateTags(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
item := ctx.Value("item").(data.GraphNode)
updateMessage := ctx.Value("message").(string)
tags, err := data.GetItemTags(item, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Error().Err(err).Msg("Error getting tags")
exceptions.ErrorPage(w, "Error getting tags")
return
}
comps := []g.Node{}
comps = append(comps, components.Tags(item, tags, true, r))
comps = append(comps, components.UpdateText(updateMessage))
comps = append(comps, components.TagList(true))
for _, comp := range comps {
err := comp.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering remove tag response")
}
}
}
package tags
import (
"context"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func RemoveTagCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
key := r.PostForm.Get("item-key")
kind := r.PostForm.Get("item-kind")
tagKey := r.PostForm.Get("tag-key")
tag, err := data.GetTagByKey(tagKey, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error getting tag")
exceptions.ErrorPage(w, "Error getting tag")
return
}
node := data.GraphNode{
Key: key,
Kind: kind,
}
err = tag.RemoveTag(node, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error removing tag")
exceptions.ErrorPage(w, "Error removing tag")
return
}
ctx = context.WithValue(r.Context(), "item", node)
ctx = context.WithValue(ctx, "message", "🏷️ Removed tag.")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package tags
import (
"context"
"net/http"
"strings"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func BulkTagCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
resultsDisplay := r.PostForm.Get("results-display")
var items []data.GraphNode
var formTags []string
for i, formValue := range r.PostForm {
if formValue[0] == "on" {
items = append(items, data.GraphNode{
Key: strings.Split(i, "-")[1],
Kind: strings.Split(i, "-")[0],
})
continue
}
if i == "bulk-tag-string" {
formTags = strings.Split(formValue[0], ",")
}
}
for i := 0; i < len(formTags); i++ {
formTag := strings.TrimSpace(formTags[i])
tag, err := data.GetTagByName(formTag, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error getting tag")
exceptions.ErrorPage(w, "Error getting tag")
return
}
// If this tag doesn't exist, create it
if tag.GraphNode.Key == "" {
tag = data.Tag{
Name: formTags[i],
GraphNode: data.GraphNode{
Key: "",
Kind: "Tag",
},
}
err := tag.Upsert(gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error upserting tag")
exceptions.ErrorPage(w, "Error upserting tag")
return
}
}
for _, item := range items {
err := tag.SetTag(item, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error setting tag")
exceptions.ErrorPage(w, "Error setting tag")
return
}
}
}
ctx = context.WithValue(r.Context(), "items", items)
ctx = context.WithValue(ctx, "results-display", resultsDisplay)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package tags
import (
"fmt"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
"github.com/rs/zerolog/log"
)
func BulkTag(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
items := ctx.Value("items").([]data.GraphNode)
resultsDisplay := ctx.Value("results-display").(string)
comps := make([]g.Node, 0)
var photos []data.Photo
for _, result := range items {
photo, err := data.GetPhotoByKey(result.Key, true, true, true, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Error().Err(err).Msg("Error in search")
return
}
if photo.ProcessedPreviews == "true" {
photos = append(photos, photo)
}
}
for _, photo := range photos {
comps = append(comps, components.ImageTile(photo, resultsDisplay, true, r),
components.Tags(photo.GraphNode, photo.Tags, true, r),
)
}
comps = append(comps, components.TagList(true))
comps = append(comps, components.UpdateText(fmt.Sprintf("🏷️ %d items tagged!", len(items))))
for _, comp := range comps {
err := comp.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering bulk tag add response")
}
}
}
package tags
import (
"context"
"net/http"
"strings"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func AddTagCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
key := r.PostForm.Get("item-key")
kind := r.PostForm.Get("item-kind")
item := data.GraphNode{
Key: key,
Kind: kind,
}
var formTags []string
for i, formValue := range r.PostForm {
if i == "tag-string" {
formTags = strings.Split(formValue[0], ",")
}
}
for i := 0; i < len(formTags); i++ {
formTag := strings.TrimSpace(formTags[i])
formTag = strings.ToLower(formTag)
tag, err := data.GetTagByName(formTag, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error getting tag")
exceptions.ErrorPage(w, "Error getting tag")
return
}
// If this tag doesn't exist, create it
if tag.GraphNode.Key == "" {
tag = data.Tag{
Name: formTag,
GraphNode: data.GraphNode{
Key: "",
Kind: "Tag",
},
}
err := tag.Upsert(gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error upserting tag")
exceptions.ErrorPage(w, "Error upserting tag")
return
}
}
err = tag.SetTag(item, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error setting tag")
exceptions.ErrorPage(w, "Error setting tag")
return
}
}
ctx = context.WithValue(r.Context(), "item", item)
ctx = context.WithValue(ctx, "message", "🏷️ Added tag.")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package sideView
import (
"context"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"github.com/go-chi/chi/v5"
)
func PhotoCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
photoKey := chi.URLParam(r, "photoKey")
p, err := data.GetPhotoByKey(photoKey, true, true, true, gm, gpart)
if err != nil || p.GraphNode.Key == "" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
ctx := context.WithValue(r.Context(), "photo", p)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package sideView
import (
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/rs/zerolog/log"
)
func Photo(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
photo := ctx.Value("photo").(data.Photo)
comp := Div(
components.SideViewPhoto(photo, false, r),
Div(ID("side-tagger"), Class(""),
FormEl(
Class("relative"),
Action("/tags/add"),
Span(Class("z-10 absolute left-0 text-lg p-2"),
g.Text("🏷️ "),
Img(ID("tag-spinner"), Class("htmx-indicator search-spinner-image absolute left-0"), Src("/css/spinner.svg")),
),
Input(Class("hidden"), Type("hidden"), Name("item-key"), Value(photo.GraphNode.Key)),
Input(Class("hidden"), Type("hidden"), Name("item-kind"), Value(photo.GraphNode.Kind)),
Input(Class("px-3 py-3 placeholder-gray-400 text-gray-600 relative bg-white bg-white rounded text-sm border border-opal-dark outline-none focus:outline-none focus:ring w-full pl-10"),
g.Attr("list", "tag-list"),
Type("text"),
Placeholder("Add tag"),
Name("tag-string"),
),
components.CsrfInputToken(r),
HxSwap("none"),
HxPost("/tags/add"),
HxIndicator("#tag-spinner"),
),
),
)
err := comp.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering photo page")
return
}
}
package web
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"net/http"
"webster/MemoryLane/global"
"webster/MemoryLane/web/captions"
"webster/MemoryLane/web/favorites"
"webster/MemoryLane/web/login"
"webster/MemoryLane/web/photos"
"webster/MemoryLane/web/search"
"webster/MemoryLane/web/security"
"webster/MemoryLane/web/sideView"
"webster/MemoryLane/web/tags"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
func Server() {
fmt.Println("\033[2J")
fmt.Println("*********************")
fmt.Println("* Running Server *")
fmt.Println("*********************")
fmt.Println("Building CSS")
cmd := exec.Command("npx", "tailwindcss", "-o", "css/tailwind.css")
stdout, err := cmd.Output()
fmt.Println(string(stdout))
if err != nil {
fmt.Println(err.Error())
return
}
r := chi.NewRouter()
r.Use(middleware.Logger)
// A good base middleware stack
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Compress(5))
// Set a timeout value on the request context (ctx), that will signal
// through ctx.Done() that the request has timed out and further
// processing should be stopped.
r.Use(middleware.Timeout(90 * time.Second))
/**************
* S E A R C H *
***************/
r.Route("/", func(r chi.Router) {
r.Use(security.SecurityCtx)
r.Use(security.CSRFMiddleware())
r.Use(search.SearchCtx)
r.Get("/", search.SearchPage)
r.Post("/results", search.Results)
})
/*******************
* S I D E V I E W *
********************/
r.Route("/side-view", func(r chi.Router) {
r.Use(security.SecurityCtx)
r.Use(security.CSRFMiddleware())
r.Route("/photo/{photoKey}", func(r chi.Router) {
r.Use(sideView.PhotoCtx)
r.Get("/", sideView.Photo)
})
})
/***********
* T A G S *
************/
r.Route("/tags", func(r chi.Router) {
r.Use(security.SecurityCtx)
r.Use(security.CSRFMiddleware())
//r.Get("/", search.SearchPage)
r.Route("/bulk-add", func(r chi.Router) {
r.Use(tags.BulkTagCtx)
r.Post("/", tags.BulkTag)
})
r.Route("/remove", func(r chi.Router) {
r.Use(tags.RemoveTagCtx)
r.Post("/", tags.UpdateTags)
})
r.Route("/add", func(r chi.Router) {
r.Use(tags.AddTagCtx)
r.Post("/", tags.UpdateTags)
})
})
/******************
* C A P T I O N S *
*******************/
r.Route("/caption", func(r chi.Router) {
r.Use(security.SecurityCtx)
r.Use(security.CSRFMiddleware())
r.Route("/form", func(r chi.Router) {
r.Use(captions.EditCaptionFormCtx)
r.Post("/", captions.EditCaptionForm)
})
r.Route("/update", func(r chi.Router) {
r.Use(captions.UpdateCaptionsCtx)
r.Post("/", captions.UpdateCaption)
})
})
/********************
* F A V O R I T E S *
*********************/
r.Route("/favorites", func(r chi.Router) {
r.Use(security.SecurityCtx)
r.Use(security.CSRFMiddleware())
r.Use(favorites.FavoritesCtx)
r.Post("/", favorites.Favorite)
})
/**************
* P H O T O S *
***************/
r.Route("/photos", func(r chi.Router) {
r.Use(security.SecurityCtx)
// Subrouters:
r.Route("/{photoKey}", func(r chi.Router) {
r.Use(photos.PhotoCtx)
r.Get("/", photos.GetPhoto) // GET /photos/123
})
})
/*************
* L O G I N *
**************/
r.Route("/login", func(r chi.Router) {
r.Use(security.CSRFMiddleware())
r.Get("/", login.LoginPage) // GET /login
r.Post("/", login.PostLogin) // POST /login
r.Get("/logout", login.Logout)
})
/***************
* S T A T I C *
****************/
workDir, _ := os.Getwd()
previews := http.Dir(filepath.Join(workDir, "previews"))
css := http.Dir(filepath.Join(workDir, "css"))
js := http.Dir(filepath.Join(workDir, "js"))
r.Route("/files", func(r chi.Router) {
r.Use(security.SecurityCtx)
FileServer(r, "/previews", previews)
})
FileServer(r, "/css", css)
FileServer(r, "/js", js)
// TODO - Port should be set from config
fmt.Printf("Starting server at port 8080\n")
s := http.Server{Addr: ":8080", Handler: r}
go s.ListenAndServe()
global.RegisterShutdownAction(func() {
log.Info().Msg("Server shutting down.")
fmt.Println("Server shutting down.")
s.Shutdown(context.Background())
})
// Keep this process running until an interrupt signal is received.
<-global.GetGlobalContext().Done()
}
// FileServer conveniently sets up a http.FileServer handler to serve
// static files from a http.FileSystem.
func FileServer(r chi.Router, path string, root http.FileSystem) {
if strings.ContainsAny(path, "{}*") {
panic("FileServer does not permit any URL parameters.")
}
if path != "/" && path[len(path)-1] != '/' {
r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP)
path += "/"
}
path += "*"
r.Get(path, func(w http.ResponseWriter, r *http.Request) {
rctx := chi.RouteContext(r.Context())
pathPrefix := strings.TrimSuffix(rctx.RoutePattern(), "/*")
fs := http.StripPrefix(pathPrefix, http.FileServer(root))
fs.ServeHTTP(w, r)
})
}
package security
import (
"context"
"fmt"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
)
func SecurityCtx(next http.Handler) http.Handler {
var ctx context.Context
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s := NewSecureCookie()
if cookie, err := r.Cookie("auth"); err == nil {
value := make(map[string]string)
if err = s.Decode("auth", cookie.Value, &value); err == nil {
user, err := data.GetUserByKey(value["key"], true, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Internal Server Error")
return
}
ctx = context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.Redirect(w, r, fmt.Sprintf("/login?redirect=%s", r.URL.Path), http.StatusFound)
})
}
package security
import "github.com/gorilla/securecookie"
var _initializedCookies = false
var _hashKey []byte
var _blockKey []byte
func NewSecureCookie() *securecookie.SecureCookie {
if !_initializedCookies {
initializeCookies()
}
return securecookie.New(_hashKey, _blockKey)
}
// TODO - This should be called on a timed basis to reset the keys
func ResetCookieKeys() {
_hashKey = securecookie.GenerateRandomKey(32)
if _hashKey == nil {
panic("Error generating hash key")
}
_blockKey = securecookie.GenerateRandomKey(32)
if _blockKey == nil {
panic("error generating block key")
}
}
func initializeCookies() {
ResetCookieKeys()
_initializedCookies = true
return
}
package security
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/spf13/viper"
)
var _initialized = false
var _csrfKey []byte
func CSRFMiddleware() func(http.Handler) http.Handler {
if !_initialized {
initializeCSRF()
}
runLevel := viper.GetString("RunLevel")
switch runLevel {
case "development":
return csrf.Protect([]byte(_csrfKey), csrf.Secure(false))
case "production":
return csrf.Protect([]byte(_csrfKey), csrf.Secure(true))
default:
return csrf.Protect([]byte(_csrfKey), csrf.Secure(true))
}
}
func initializeCSRF() {
_csrfKey = []byte(viper.GetString("CSRFKey"))
_initialized = true
}
package search
import (
"fmt"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/web/components"
"webster/MemoryLane/web/layouts"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func SearchPage(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := ctx.Value("user").(data.User)
radioPanelButtons := []components.RadioPanelButton{
{Name: "small", Label: "small", Value: "small", Checked: true},
{Name: "medium", Label: "medium", Value: "medium", Checked: false},
{Name: "polaroid", Label: "polaroid", Value: "polaroid", Checked: false},
}
b := Div(Class("w-full space-y-4"),
Div(Class("flew w-full"),
Div(Class("flex p-5 w-full"),
FormEl(
Class("flex-grow w-full bulk-form relative"),
Action("/results"),
Span(Class("z-10 absolute left-0 text-lg p-2"),
g.Text("🔍"),
Img(ID("search-spinner"), Class("htmx-indicator search-spinner-image absolute left-0"), Src("/css/spinner.svg")),
),
Input(Class("px-3 py-3 pl-10 placeholder-gray-light text-dark relative bg-white bg-white rounded text-sm border border-opal-dark outline-none focus:outline-none focus:ring w-full"),
Type("text"),
Placeholder(fmt.Sprintf("Hello %s, search here to wander Memory Lane", user.UserName)),
Name("search-string"),
),
components.CsrfInputToken(r),
Div(Class("float-right p-4"),
components.RadioPanel("results-display", radioPanelButtons, "/results", "#search-results", "#search-spinner"),
//components.Toggle("size-medium", "small", "large", "/results", "#search-results", "#search-spinner"),
),
Div(ID("bulk-tagger")),
HxPost("/results"),
HxTarget("#search-results"),
HxIndicator("#search-spinner"),
),
),
),
Div(Class(""),
Div(Class("flex p-5 z-10 bulk-form"),
ID("search-results"),
),
),
)
_ = layouts.SidebarView("Search", r.URL.Path, b, nil, true).Render(w)
}
package search
import (
"context"
"time"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func SearchCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
var results = []data.GraphNode{}
var err error
t1 := time.Now()
err = r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
searchString := r.PostForm.Get("search-string")
resultsDisplay := r.PostForm.Get("results-display")
user := r.Context().Value("user").(data.User)
results, err = data.Search(searchString, nil, user, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx = context.WithValue(r.Context(), "results", results)
ctx = context.WithValue(ctx, "results-display", resultsDisplay)
ctx = context.WithValue(ctx, "t1", t1)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package search
import (
"fmt"
"net/http"
"sort"
"time"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/rs/zerolog/log"
)
func Results(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
results := ctx.Value("results").([]data.GraphNode)
resultsDisplay := ctx.Value("results-display").(string)
t1 := ctx.Value("t1").(time.Time)
tiles := make([]g.Node, 0)
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
//TODO Eventually this will need to track items that aren't just photos.
var photos []data.Photo
for _, result := range results {
photo, err := data.GetPhotoByKey(result.Key, true, true, true, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error in search")
return
}
if photo.ProcessedPreviews == "true" {
photos = append(photos, photo)
}
}
sort.SliceStable(photos, func(i, j int) bool {
return photos[i].DateTaken.After((photos[j].DateTaken))
})
for _, photo := range photos {
tiles = append(tiles, components.ImageTile(photo, resultsDisplay, false, r))
}
comps := []g.Node{}
comps = append(comps, g.Group(tiles),
components.TagList(true),
components.UpdateText(fmt.Sprintf("🔍 %d results found in %s.", len(photos), time.Since(t1).String())),
)
if len(photos) > 0 {
comps = append(comps, BulkTagger(true, r))
}
b := Div(Class("flex-grow"),
g.Group(comps),
)
w.Header().Set("Content-Type", "text/html")
err := b.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error in search")
return
}
}
package search
import (
"net/http"
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func BulkTagger(oobSwap bool, r *http.Request) (comp g.Node) {
oob := ""
if oobSwap {
oob = "outerHTML:#bulk-tagger"
}
comp = Div(ID("bulk-tagger"), Class("flex-grow p-2 w-1/2 float-left"),
HxSwapOob(oob),
FormEl(
Class("flex-grow w-full bulk-form relative"),
Action("/tags/bulk-add"),
Span(Class("z-10 absolute left-0 text-lg p-2"),
g.Text("🏷️ "),
Img(ID("tag-spinner"), Class("htmx-indicator search-spinner-image absolute left-0"), Src("/css/spinner.svg")),
),
Input(Class("px-3 py-3 placeholder-gray-light text-gray-dark relative bg-white bg-white rounded text-sm border border-opal-dark outline-none focus:outline-none focus:ring w-full pl-10"),
g.Attr("list", "tag-list"),
Type("text"),
Placeholder("Add tag to selected images"),
Name("bulk-tag-string"),
),
components.CsrfInputToken(r),
HxSwap("none"),
HxInclude(".bulk-form"),
HxPost("/tags/bulk-add"),
HxIndicator("#tag-spinner"),
),
)
return
}
package photos
import (
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/web/components"
"webster/MemoryLane/web/layouts"
g "github.com/maragudk/gomponents"
. "github.com/maragudk/gomponents/html"
"github.com/rs/zerolog/log"
)
func GetPhoto(w http.ResponseWriter, r *http.Request) {
log.Debug().Msg("Building Photo Page")
ctx := r.Context()
photo := ctx.Value("photo").(data.Photo)
b := Div(Class("container mx-auto"),
H1(g.Textf("Photo Page for %s", photo.GraphNode.Name)),
components.CardWithImage(photo),
)
_ = layouts.SidebarView("Photo", r.URL.Path, b, nil, true).Render(w)
}
package photos
import (
"context"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"github.com/go-chi/chi/v5"
)
func PhotoCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
photoKey := chi.URLParam(r, "photoKey")
p, err := data.GetPhotoByKey(photoKey, true, true, true, gm, gpart)
if err != nil || p.GraphNode.Key == "" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
ctx := context.WithValue(r.Context(), "photo", p)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package login
import (
"net/http"
"time"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/components"
"webster/MemoryLane/web/exceptions"
"webster/MemoryLane/web/layouts"
"webster/MemoryLane/web/security"
g "github.com/christophersw/gomponents-htmx"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
. "github.com/christophersw/gomponents-htmx/html"
)
func LoginPage(w http.ResponseWriter, r *http.Request) {
b := Div(Class("flex flex-col items-center justify-center min-h-screen bg-gray"),
FormEl(Class("bg-gray-paper w-full sm:w-3/4 max-w-lg p-12 pb-6 shadow-2xl rounded"),
Action(r.RequestURI),
Method("post"),
Div(Class("text-gray-dark pb-4 text-3xl font-semibold"), g.Text("Memory Lane")),
Input(Class("block text-gray p-1 m-4 ml-0 w-full rounded text-lg font-normal placeholder-gray-lighter active:border-opal required:border-gold invalid:border-red valid:border-opal-dark"),
Name("username"),
ID("username"),
Type("text"),
Placeholder("your username"),
Required(),
),
Input(Class("block text-gray p-1 m-4 ml-0 w-full rounded text-lg font-normal placeholder-gray-lighter required:border-gold invalid:border-red valid:border-opal-dark"),
Name("password"),
ID("password"),
Type("password"),
Placeholder("your password"),
Required(),
),
components.CsrfInputToken(r),
Button(Class("inline-block mt-2 bg-opal-dark hover:bg-opal focus:bg-opal px-6 py-2 rounded text-gray-dark shadow-lg"),
g.Text("Login"),
Type("submit"),
),
),
)
_ = layouts.EmptyPage("Login", r.URL.Path, b).Render(w)
}
func LoginPageWithMessage(w http.ResponseWriter, r *http.Request, message string) {
b := Div(Class("flex flex-col items-center justify-center min-h-screen bg-gray"),
FormEl(Class("bg-gray-paper w-full sm:w-3/4 max-w-lg p-12 pb-6 shadow-2xl rounded"),
Action(r.URL.Path),
Method("post"),
components.WarningAlert(message),
Div(Class("text-gray-dark pb-4 text-3xl font-semibold"), g.Text("Memory Lane")),
Input(Class("block text-gray-black p-1 m-4 ml-0 w-full rounded text-lg font-normal placeholder-gray-300"),
Name("username"),
ID("username"),
Type("text"),
Placeholder("your username"),
),
Input(Class("block text-gray-black p-1 m-4 ml-0 w-full rounded text-lg font-normal placeholder-gray-300"),
Name("password"),
ID("password"),
Type("password"),
Placeholder("your password"),
),
components.CsrfInputToken(r),
Button(Class("inline-block mt-2 bg-opal-dark hover:bg-opal focus:bg-opal px-6 py-2 rounded text-gray-dark shadow-lg"),
g.Text("Login"),
Type("submit"),
),
),
)
_ = layouts.EmptyPage("Login", r.URL.Path, b).Render(w)
}
func PostLogin(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
redirect := query.Get("redirect")
if redirect == "" {
redirect = "/"
}
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
exceptions.ErrorPage(w, "Error reading body")
return
}
username := r.PostForm.Get("username")
password := r.PostForm.Get("password")
if username == "" {
LoginPageWithMessage(w, r, "Missing username")
return
}
if password == "" {
LoginPageWithMessage(w, r, "Missing password")
return
}
u, err := data.GetUserByUsername(username, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
exceptions.ErrorPage(w, "Error reading body")
return
}
if u.GraphNode.Key == "" {
log.Warn().Msgf("Failed Login Attempt, user %s does not exist", username)
LoginPageWithMessage(w, r, "Invalid username or password")
return
}
match, err := u.CheckPassword(password, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Warn().Msgf("Failed Login Attempt, wrong password for user %s ", username)
LoginPageWithMessage(w, r, "Invalid username or password")
return
}
if match {
s := security.NewSecureCookie()
value := map[string]string{
"username": u.UserName,
"key": u.GraphNode.Key,
}
if encoded, err := s.Encode("auth", value); err == nil {
runLevel := viper.GetString("RunLevel")
secure := true
switch runLevel {
case "development":
secure = false
case "production":
secure = true
}
cookie := &http.Cookie{
Name: "auth",
Value: encoded,
Path: "/",
Secure: secure,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
http.Redirect(w, r, redirect, http.StatusFound)
return
}
}
func Logout(w http.ResponseWriter, r *http.Request) {
c := &http.Cookie{
Name: "auth",
Value: "",
Path: "/",
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: true,
}
http.SetCookie(w, c)
http.Redirect(w, r, "/", http.StatusFound)
}
package layouts
import (
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
c "github.com/christophersw/gomponents-htmx/components"
. "github.com/christophersw/gomponents-htmx/html"
)
func SidebarView(title, path string, body g.Node, sideView g.Node, usesTags bool) g.Node {
lists := []g.Node{}
if usesTags {
lists = append(lists, components.TagList(false))
}
// HTML5 boilerplate document
return c.HTML5(c.HTML5Props{
Title: title,
Language: "en",
Head: head(),
Body: []g.Node{
// See https://play.tailwindcss.com/uOnWQzR9tl
Main(Class("flex flex-col h-screen"),
Div(Class("flex flex-1 overflow-hidden"),
Div(Class("flex-col w-1/3 p-4 bg-gray-lightest drop-shadow-xl resize-x overflow-y-auto"),
// Sidebar
Div(Class(""),
Div(Class("h-1/6"), H1(Class("text-gold-dark"), g.Text("Memory Lane"))),
),
Div(ID("side-view"), Class("flex w-full"),
sideView,
),
),
Div(Class("flex flex-1 flex-col"),
Div(Class("flex h-16 p-4 bg-opal-light "),
// Header
components.Top(),
),
Div(Class("flex flex-1 bg-gray-paper overflow-y-auto paragraph px-4"),
// Body
body,
),
Div(Class("flex w-full bg-opal-light"),
// Bottom
components.Bottom(),
),
),
),
g.Group(lists),
),
},
})
}
func EmptyPage(title, path string, body g.Node) g.Node {
// HTML5 boilerplate document
return c.HTML5(c.HTML5Props{
Title: title,
Language: "en",
Head: head(),
Body: []g.Node{
body,
},
})
}
func head() []g.Node {
return []g.Node{
Script(Src("/js/htmx.min.js")),
//Script(Src("https://unpkg.com/htmx.org@1.6.1")),
//Script(Type("module"), Src("https://unpkg.com/blurhash-img?module")),
Script(Src("https://unpkg.com/hyperscript.org@0.9.1")),
Link(Href("/css/tailwind.css"), Rel("stylesheet")),
Link(Href("/css/custom.css"), Rel("stylesheet")),
//Link(Href("https://cdn.jsdelivr.net/npm/@tailwindcss/custom-forms@0.2.1/dist/custom-forms.css"), Rel("stylesheet")),
}
}
package favorites
import (
"context"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func FavoritesCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
user := ctx.Value("user").(data.User)
key := r.PostForm.Get("item-key")
kind := r.PostForm.Get("item-kind")
node := data.GraphNode{
Key: key,
Kind: kind,
}
ctx = context.WithValue(ctx, "item", node)
isFav := false
userFavs, err := user.GetFavorites(node.Kind, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error getting favorites")
exceptions.ErrorPage(w, "Error getting favorites")
return
}
for _, fav := range userFavs {
if fav.Key == node.Key {
isFav = true
break
}
}
if isFav {
err = user.RemoveFavorite(node, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error removing favorite")
exceptions.ErrorPage(w, "Error removing favorite")
return
}
ctx = context.WithValue(ctx, "message", "❤️ Unfavorited.")
} else {
err = user.SetFavorite(node, gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error adding favorite")
exceptions.ErrorPage(w, "Error adding favorite")
return
}
ctx = context.WithValue(ctx, "message", "❤️ Favorited.")
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package favorites
import (
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/web/components"
g "github.com/christophersw/gomponents-htmx"
"github.com/rs/zerolog/log"
)
func Favorite(w http.ResponseWriter, r *http.Request) {
item := r.Context().Value("item").(data.GraphNode)
message := r.Context().Value("message").(string)
comps := []g.Node{
components.FavoriteButton(item, true, r),
components.UpdateText(message),
}
for _, comp := range comps {
err := comp.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering favorite response")
}
}
}
package exceptions
import (
"net/http"
)
func ErrorPage(w http.ResponseWriter, message string) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("<html><body><h1>" + message + "</h1></body></html>"))
}
package components
import (
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func Top() (comp g.Node) {
comp =
Div(Class("flex flex-row-reverse w-full"),
Div(
A(Class("bg-gold text-xs hover:bg-gold-light text-gray-dark object-right font-bold py-2 px-4 rounded"),
g.Text("logout"),
Href("/login/logout"),
),
),
)
return
}
package components
import (
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func Toggle(id string, leftLabel string, rightLabel string, url string, target string, indicator string) (comp g.Node) {
comp = Div(Class("flex items-center justify-right w-full"),
Div(Class("ml-3 text-gray-700 font-medium"),
g.Text(leftLabel),
),
Label(For(id), Class("flex items-center cursor-pointer"),
// Toggle
Div(Class("relative"),
// Input
Input(ID(id), Name(id), Type("checkbox"), Class("sr-only"),
g.Attr("hx-post", url),
g.Attr("hx-target", target),
g.Attr("hx-trigger", "change"),
g.Attr("hx-indicator", indicator),
),
// Line
Div(Class("w-10 h-4 bg-gray-400 rounded-full shadow-inner")),
// Dot
Div(Class("dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition")),
),
),
// Label
Div(Class("ml-3 text-gray-700 font-medium"),
g.Text(rightLabel),
),
)
return
}
package components
import (
"fmt"
"net/http"
"sort"
"strings"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func Tags(item data.GraphNode, tags []data.Tag, oobSwap bool, r *http.Request) (comp g.Node) {
tagNodes := []g.Node{}
sort.SliceStable(tags, func(i, j int) bool {
nameI := strings.ToLower(tags[i].Name)
nameJ := strings.ToLower(tags[j].Name)
return nameI < nameJ
})
for _, tag := range tags {
tagNodes = append(tagNodes, Tag(item, tag, r))
}
className := fmt.Sprintf("tags-%s", item.Key)
oob := ""
if oobSwap {
oob = "outerHTML:." + className
}
comp = Div(Class(className+" text-xs font-mono text-gray h-20 overflow-scroll"),
HxSwapOob(oob),
P(Class("ml-2"), g.Textf("%d tags", len(tags))),
g.Group(tagNodes),
)
return comp
}
func Tag(item data.GraphNode, tag data.Tag, r *http.Request) (comp g.Node) {
comp = Div(Class("tag rounded inline-block bg-opal-dark text-white p-1 m-2 relative"),
A(Class("tag-link"), Href("#"), g.Text(tag.Name)),
FormEl(
Input(Class("hidden"), Type("hidden"), Name("item-key"), Value(item.Key)),
Input(Class("hidden"), Type("hidden"), Name("item-kind"), Value(item.Kind)),
Input(Class("hidden"), Type("hidden"), Name("tag-name"), Value(tag.GraphNode.Name)),
Input(Class("hidden"), Type("hidden"), Name("tag-key"), Value(tag.GraphNode.Key)),
Button(Class("tag-remove"), g.Text("x"),
Span(Class("rounded-full bg-white absolute top-0 right-0 htmx-indicator"), ID(fmt.Sprintf("%s-%s-spinner", item.Key, tag.GraphNode.Key)),
Img(Src("/css/spinner.svg")),
),
HxIndicator(fmt.Sprintf("#%s-%s-spinner", item.Key, tag.GraphNode.Key)),
HxPost("/tags/remove"),
CsrfInputToken(r),
),
),
)
return
}
func TagList(oobSwap bool) (comp g.Node) {
tags, err := data.GetAllTags(graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
panic(err)
}
oob := ""
if oobSwap {
oob = "#tag-list"
}
tagOptions := []g.Node{}
for _, tag := range tags {
tagOptions = append(tagOptions, Option(Label(g.Text(tag.Name)), Value(tag.Name)))
}
comp = DataList(ID("tag-list"), HxSwapOob(oob), g.Group(tagOptions))
return
}
package components
import (
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
type RadioPanelButton struct {
Name string
Value string
Label string
Checked bool
}
func RadioPanel(name string, buttons []RadioPanelButton, url string, target string, indicator string) (comp g.Node) {
radioButtons := []g.Node{}
for _, button := range buttons {
checked := ""
if button.Checked {
checked = "checked"
}
radioButton := Label(Class("radio-panel-button"),
Span(Class("radio-panel-label"), g.Text(button.Label)),
Input(ID(button.Name), Name(name), Value(button.Value), Type("radio"), g.Attr(checked),
g.Attr("hx-post", url),
g.Attr("hx-target", target),
g.Attr("hx-trigger", "change"),
g.Attr("hx-indicator", indicator)),
Span(Class("radio-panel-checkmark")),
)
radioButtons = append(radioButtons, radioButton)
}
comp = Div(ID("radio-panel-"+name), Class("radio-panel"),
Div(Class("radio-panel-track")),
g.Group(radioButtons))
return
}
package components
import (
"fmt"
"net/http"
"webster/MemoryLane/data"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/rs/zerolog/log"
)
func CardWithImage(photo data.Photo) (comp g.Node) {
log.Debug().Msg("Building Photo Page")
comp = Div(Class("bg-white shadow-md border border-gray-200 rounded-lg max-w-sm dark:bg-gray-800 dark:border-gray-700"),
Img(Class("rounded-t-lg"),
Src(fmt.Sprintf("/files/previews/medium/%s.webp", photo.GraphNode.Key)),
g.Raw(fmt.Sprintf("-blurhash=\"%s\"", photo.BlurHash)),
),
Div(Class("p-5"),
H5(Class("text-gray-900 font-bold text-2xl tracking-tight mb-2 dark:text-white"),
g.Textf("Photo %s", photo.GraphNode.Key),
),
P(Class("font-normal text-gray-700 mb-3 dark:text-gray-400"),
Ul(
Li(g.Textf("Date Taken: %s", photo.DateTaken)),
Li(g.Textf("Lens: %s", photo.LensInfo)),
Li(g.Textf("Make: %s", photo.Make)),
Li(g.Textf("Model: %s", photo.Model)),
Li(g.Textf("GPS: %g, %g", photo.Longitude, photo.Latitude)),
),
),
),
)
return
}
func ImageTile(photo data.Photo, displayType string, oobSwap bool, r *http.Request) (comp g.Node) {
switch displayType {
case "small":
return SmallTile(photo, oobSwap, r)
case "medium":
return MediumTile(photo, oobSwap, r)
case "polaroid":
return PolaroidTile(photo, oobSwap, r)
default:
return SmallTile(photo, oobSwap, r)
}
}
func SmallTile(photo data.Photo, oobSwap bool, r *http.Request) (comp g.Node) {
const compType = "small-tile"
src := fmt.Sprintf("/files/previews/square-small/%s.webp", photo.GraphNode.Key)
oob := ""
if oobSwap {
oob = "true"
}
comp = Div(ID(getId(compType, photo)), Class("inline-block relative p-2"), HxSwapOob(oob),
Span(Class("absolute top-2.5 right-3 z-50 w-4"),
FavoriteButton(photo.GraphNode, false, r),
),
Input(Class("absolute top-1 left-1 m-2 z-50"), Type("checkbox"), Name(fmt.Sprintf("photo-%s", photo.GraphNode.Key))),
A(Href("#"),
//g.Raw(fmt.Sprintf("<blurhash-img hash=\"%s\" id=\"blur-%s\" class=\"rounded max-w-full h-auto align-middle border-none\" style=\"width:100px; --aspect-ratio: 4/4\"></blurhash-img>", photo.BlurHash, photo.GraphNode.Key)),
Img(Class("rounded max-w-full h-auto align-middle border-none"),
Src(src),
Loading("lazy"),
//HxTrigger("load"),
//g.Attr("_", fmt.Sprintf("on load hide #blur-%s then show me", photo.GraphNode.Key)),
),
P(Class("text-xs"), g.Text(photo.DateTaken.Format("Jan 2, 2006"))),
HxTrigger("click"),
HxGet(fmt.Sprintf("/side-view/photo/%s", photo.GraphNode.Key)),
HxTarget("#side-view"),
HxIndicator("#side-preview-spinner"),
),
)
return
}
func MediumTile(photo data.Photo, oobSwap bool, r *http.Request) (comp g.Node) {
const compType = "medium-tile"
src := fmt.Sprintf("/files/previews/square-medium/%s.webp", photo.GraphNode.Key)
oob := ""
if oobSwap {
oob = "true"
}
comp = Div(ID(getId(compType, photo)), Class("inline-block relative p-2"), HxSwapOob(oob),
Span(Class("absolute top-2.5 right-3 z-50 w-4"),
FavoriteButton(photo.GraphNode, false, r),
),
Input(Class("absolute top-1 left-1 m-2 z-50"), Type("checkbox"), Name(fmt.Sprintf("photo-%s", photo.GraphNode.Key))),
A(Href("#"),
//g.Raw(fmt.Sprintf("<blurhash-img hash=\"%s\" id=\"blur-%s\" class=\"rounded max-w-full h-auto align-middle border-none\" style=\"width:100px; --aspect-ratio: 4/4\"></blurhash-img>", photo.BlurHash, photo.GraphNode.Key)),
Img(Class("rounded max-w-full h-auto align-middle border-none"),
Src(src),
Loading("lazy"),
//HxTrigger("load"),
//g.Attr("_", fmt.Sprintf("on load hide #blur-%s then show me", photo.GraphNode.Key)),
),
P(Class("text-xs"), g.Text(photo.DateTaken.Format("Jan 2, 2006"))),
HxTrigger("click"),
HxGet(fmt.Sprintf("/side-view/photo/%s", photo.GraphNode.Key)),
HxTarget("#side-view"),
HxIndicator("#side-preview-spinner"),
),
)
return
}
func PolaroidTile(photo data.Photo, oobSwap bool, r *http.Request) (comp g.Node) {
const compType = "polaroid-tile"
src := fmt.Sprintf("/files/previews/square-medium/%s.webp", photo.GraphNode.Key)
oob := ""
if oobSwap {
oob = "true"
}
comp = Div(ID(getId(compType, photo)), Class("inline-block bg-white w-80 rounded overflow-hidden shadow-2xl relative p-2 m-4"), HxSwapOob(oob),
Span(Class("absolute top-2.5 right-3 z-50 w-4"),
FavoriteButton(photo.GraphNode, false, r),
),
Input(Class("absolute top-1 left-1 m-2 z-50"), Type("checkbox"), Name(fmt.Sprintf("photo-%s", photo.GraphNode.Key))),
A(Href("#"), Class("z-10 relative"),
//g.Raw(fmt.Sprintf("<blurhash-img hash=\"%s\" id=\"blur-%s\" class=\"rounded max-w-full h-auto align-middle border-none\" style=\"width:100px; --aspect-ratio: 4/4\"></blurhash-img>", photo.BlurHash, photo.GraphNode.Key)),
Img(Class("rounded w-full h-auto align-middle border-none"),
Src(src),
Loading("lazy"),
//HxTrigger("load"),
//g.Attr("_", fmt.Sprintf("on load hide #blur-%s then show me", photo.GraphNode.Key)),
),
HxTrigger("click"),
HxGet(fmt.Sprintf("/side-view/photo/%s", photo.GraphNode.Key)),
HxTarget("#side-view"),
HxIndicator("#side-preview-spinner"),
),
P(Class("text-xs font-mono float-right text-gray"), g.Textf("%s", photo.DateTaken.Format("Jan 2, 2006"))),
Div(Class("w-full h-20 z-50 overflow-y-scroll"),
DisplayCaption(photo.GraphNode, photo.Caption, false, r),
),
Div(Class("text-xs font-mono text-gray w-full h-20 p-1 overflow-scroll"),
Tags(photo.GraphNode, photo.Tags, false, r),
), //TODO Fix this when tags searching is added, should be a link
)
return
}
func SideViewPhoto(photo data.Photo, oobSwap bool, r *http.Request) (comp g.Node) {
const compType = "sideview-photo"
src := fmt.Sprintf("/files/previews/full/%s.webp", photo.GraphNode.Key)
oob := ""
if oobSwap {
oob = "true"
}
comp = Div(ID(getId(compType, photo)), Class("bg-white w-full rounded relative overflow-hidden drop-shadow p-2"), HxSwapOob(oob),
Img(Class("rounded w-full h-auto align-middle border-none"),
Src(src),
Loading("lazy"),
),
Span(Class("absolute top-3.5 right-4 z-50 w-6"),
FavoriteButton(photo.GraphNode, false, r),
),
Div(Class("px-6 py-4 h-30 overflow-scroll"),
P(Class("text-xs font-mono float-right text-gray"), g.Textf("%s", photo.DateTaken.Format("Jan 2, 2006"))),
Div(Class("flex w-full"),
DisplayCaption(photo.GraphNode, photo.Caption, false, r),
),
),
Div(Class("text-xs font-mono text-gray h-20 overflow-scroll"),
Tags(photo.GraphNode, photo.Tags, false, r),
), //TODO Fix this when tags searching is added, should be a link
)
return
}
func getId(componentType string, photo data.Photo) string {
return fmt.Sprintf("%s-%s", componentType, photo.GraphNode.Key)
}
package components
import (
"fmt"
"net/http"
"webster/MemoryLane/data"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/teris-io/shortid"
)
func EditCaption(item data.GraphNode, caption data.Caption, r *http.Request) (comp g.Node) {
className := fmt.Sprintf("caption-box-%s", item.Key)
id := fmt.Sprintf("caption-%s-%s", item.Key, shortid.MustGenerate())
comp = FormEl(ID(id), Class(className+" w-full h-full"),
Input(Class("hidden"), Type("hidden"), Name("item-key"), Value(item.Key)),
Input(Class("hidden"), Type("hidden"), Name("item-kind"), Value(item.Kind)),
Textarea(Name("caption-text"), Class("w-full h-full"), MaxLength("100"),
AutoFocus(),
HxPost("/caption/update"),
HxTrigger("keyup changed delay:3s"),
g.Text(caption.Text),
),
CsrfInputToken(r),
)
return
}
func DisplayCaption(item data.GraphNode, caption data.Caption, oobSwap bool, r *http.Request) (comp g.Node) {
className := fmt.Sprintf("caption-box-%s", item.Key)
helpText := P()
if caption.Text == "" {
helpText = P(Class("text-gray-light font-xs"), g.Text("click to caption"))
}
id := fmt.Sprintf("caption-%s-%s", item.Key, shortid.MustGenerate())
oob := ""
if oobSwap {
oob = "outerHTML:." + className
}
comp = FormEl(Class(className+" w-full h-full"), ID(id),
P(Class("w-full h-full"), g.Text(caption.Text)),
helpText,
HxTrigger("click"),
HxPost("/caption/form"),
Input(Class("hidden"), Type("hidden"), Name("replace"), Value(id)),
Input(Class("hidden"), Type("hidden"), Name("item-key"), Value(item.Key)),
Input(Class("hidden"), Type("hidden"), Name("item-kind"), Value(item.Kind)),
CsrfInputToken(r),
HxTarget("#"+id),
HxSwap("outerHTML"),
HxSwapOob(oob),
)
return
}
package components
import (
"fmt"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/rs/zerolog/log"
)
func FavoriteButton(item data.GraphNode, oobSwap bool, r *http.Request) (comp g.Node) {
user := r.Context().Value("user").(data.User)
isFav := false
favorites, err := user.GetFavorites(item.Kind, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Error().Err(err).Msg("Error adding favorite")
panic(err)
}
for _, fav := range favorites {
if fav.Key == item.Key {
isFav = true
break
}
}
className := fmt.Sprintf("%s-favorite-form", item.Key)
oob := ""
if oobSwap {
oob = "outerHTML:." + className
}
if isFav {
comp = FormEl(Class(className),
CsrfInputToken(r),
Input(Type("hidden"), Name("item-key"), Value(item.Key)),
Input(Type("hidden"), Name("item-kind"), Value(item.Kind)),
Button(
g.Raw(`<svg viewBox="0 0 128 128" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 128 128"><path d="M39.9 96.4c-1.1-1.3-2.7-2.8-3.9-4l-7.9-7.9c-4.1-4-8.3-8.2-12.5-12.2C5.9 66.1 0 55.4 0 43.8c0-18.8 15.2-34 34-34 12.9 0 24.2 7.2 30 18 5.7-10.7 17.1-18 30-18 18.8 0 34 15.2 34 34 0 11.6-5.9 22.3-15.6 28.6-4.2 3.9-8.4 8.1-12.5 12.2-2.7 2.6-5.3 5.2-7.9 7.9-1.2 1.2-2.8 2.7-3.9 4-.2.3-.4.6-.7.9l-20.6 21c-.8.8-1.7 1.2-2.9 1.2-1.1 0-2.1-.4-2.9-1.2l-20.6-21c-.1-.4-.3-.7-.5-1zM64 115.5l20.6-21c-.8 0 25.3-25.3 25.4-25.3 8.4-5.3 14-14.7 14-25.4 0-16.6-13.4-30-30-30-16.2 0-29.4 12.8-30 28.8-.6-16-13.8-28.8-30-28.8-16.6 0-30 13.4-30 30 0 10.7 5.6 20.1 14 25.4.1 0 26.2 25.3 25.4 25.3l20.6 21z" fill-rule="evenodd" clip-rule="evenodd" fill="#ED5C5A" class="fill-000000"></path><path d="m64 115.5-20.6-21c.8 0-25.3-25.3-25.4-25.3C9.6 63.9 4 54.5 4 43.8c0-16.6 13.4-30 30-30 16.2 0 29.4 12.8 30 28.8.6-16 13.8-28.8 30-28.8 16.6 0 30 13.4 30 30 0 10.7-5.6 20.1-14 25.4-.1 0-26.2 25.3-25.4 25.3l-20.6 21z" fill-rule="evenodd" clip-rule="evenodd" fill="#ED5C5A" class="fill-7aced7"></path></svg>`),
Span(Class("rounded-full bg-white absolute top-0 right-0 htmx-indicator"), ID(fmt.Sprintf("%s-favorite-spinner", item.Key)),
Img(Src("/css/spinner.svg")),
),
),
HxPost("/favorites"),
HxIndicator(fmt.Sprintf("#%s-favorite-spinner", item.Key)),
HxSwapOob(oob),
)
} else {
comp = FormEl(Class(className),
CsrfInputToken(r),
Input(Type("hidden"), Name("item-key"), Value(item.Key)),
Input(Type("hidden"), Name("item-kind"), Value(item.Kind)),
Button(
g.Raw(`<svg viewBox="0 0 128 128" width="100%" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 128 128"><path d="M39.9 96.4c-1.1-1.3-2.7-2.8-3.9-4l-7.9-7.9c-4.1-4-8.3-8.2-12.5-12.2C5.9 66.1 0 55.4 0 43.8c0-18.8 15.2-34 34-34 12.9 0 24.2 7.2 30 18 5.7-10.7 17.1-18 30-18 18.8 0 34 15.2 34 34 0 11.6-5.9 22.3-15.6 28.6-4.2 3.9-8.4 8.1-12.5 12.2-2.7 2.6-5.3 5.2-7.9 7.9-1.2 1.2-2.8 2.7-3.9 4-.2.3-.4.6-.7.9l-20.6 21c-.8.8-1.7 1.2-2.9 1.2-1.1 0-2.1-.4-2.9-1.2l-20.6-21c-.1-.4-.3-.7-.5-1zM64 115.5l20.6-21c-.8 0 25.3-25.3 25.4-25.3 8.4-5.3 14-14.7 14-25.4 0-16.6-13.4-30-30-30-16.2 0-29.4 12.8-30 28.8-.6-16-13.8-28.8-30-28.8-16.6 0-30 13.4-30 30 0 10.7 5.6 20.1 14 25.4.1 0 26.2 25.3 25.4 25.3l20.6 21z" fill-rule="evenodd" clip-rule="evenodd" fill="#34425B" class="fill-000000"></path><path d="m64 115.5-20.6-21c.8 0-25.3-25.3-25.4-25.3C9.6 63.9 4 54.5 4 43.8c0-16.6 13.4-30 30-30 16.2 0 29.4 12.8 30 28.8.6-16 13.8-28.8 30-28.8 16.6 0 30 13.4 30 30 0 10.7-5.6 20.1-14 25.4-.1 0-26.2 25.3-25.4 25.3l-20.6 21z" fill-rule="evenodd" clip-rule="evenodd" fill="#ffffff" class="fill-7aced7"></path></svg>`),
Span(Class("rounded-full bg-white absolute top-0 right-0 htmx-indicator"), ID(fmt.Sprintf("%s-favorite-spinner", item.Key)),
Img(Src("/css/spinner.svg")),
),
),
HxPost("/favorites"),
HxIndicator(fmt.Sprintf("#%s-favorite-spinner", item.Key)),
HxSwapOob(oob),
)
}
return
}
package components
import (
"net/http"
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
"github.com/gorilla/csrf"
)
func CsrfInputToken(r *http.Request) g.Node {
return Input(Name("gorilla.csrf.Token"), Type("hidden"), Value(csrf.Token(r)))
}
package components
import (
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
func Bottom() (comp g.Node) {
comp = Div(Class("flex w-full p-2"),
Img(ID("side-preview-spinner"), Class("htmx-indicator w-9 p-2 object-right"), Src("/css/spinner.svg")),
UpdateText("👋 Hello."),
)
return
}
func UpdateText(message string) (comp g.Node) {
comp = Div(ID("update-text"), Class("w-full text-xs font-mono text-gray"),
P(Class("float-right"), g.Text(message)),
HxSwapOob("true"),
)
return
}
package components
import (
g "github.com/christophersw/gomponents-htmx"
. "github.com/christophersw/gomponents-htmx/html"
)
// see https://tailwind-elements.com/docs/standard/components/alerts/
func WarningAlert(msg string) (comp g.Node) {
comp = Div(Class("my-3 block text-sm text-left text-gray-black bg-gold h-12 flex items-center p-4 rounded-md shadow-lg"),
Role("alert"),
g.Text(msg),
)
return
}
package captions
import (
"context"
"fmt"
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/exceptions"
"github.com/rs/zerolog/log"
)
func UpdateCaptionsCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
key := r.PostForm.Get("item-key")
kind := r.PostForm.Get("item-kind")
text := r.PostForm.Get("caption-text")
item := data.GraphNode{
Key: key,
Kind: kind,
}
caption := data.Caption{
Text: text,
CaptionedItem: item,
}
err = caption.Upsert(gm, gpart)
if err != nil {
log.Error().Err(err).Msg("Error adding caption")
exceptions.ErrorPage(w, "Error adding caption")
return
}
ctx = context.WithValue(r.Context(), "item", item)
ctx = context.WithValue(ctx, "message", fmt.Sprintf("📝 Caption updated for %s.", item.Key))
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func EditCaptionFormCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx context.Context
err := r.ParseForm()
if err != nil {
log.Error().Err(err).Msg("Error parsing form")
exceptions.ErrorPage(w, "Error parsing form")
return
}
key := r.PostForm.Get("item-key")
kind := r.PostForm.Get("item-kind")
replace := r.PostForm.Get("replace")
item := data.GraphNode{
Key: key,
Kind: kind,
}
ctx = context.WithValue(r.Context(), "item", item)
ctx = context.WithValue(ctx, "replace", replace)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
package captions
import (
"net/http"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web/components"
"webster/MemoryLane/web/exceptions"
g "github.com/christophersw/gomponents-htmx"
"github.com/rs/zerolog/log"
)
func UpdateCaption(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
item := ctx.Value("item").(data.GraphNode)
updateMessage := ctx.Value("message").(string)
caption, err := data.GetItemCaption(item, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Error().Err(err).Msg("Error getting caption")
exceptions.ErrorPage(w, "Error getting caption")
return
}
comps := []g.Node{}
comps = append(comps, components.DisplayCaption(item, caption, true, r))
comps = append(comps, components.UpdateText(updateMessage))
for _, comp := range comps {
err := comp.Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering caption response")
}
}
}
func EditCaptionForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
item := ctx.Value("item").(data.GraphNode)
caption, err := data.GetItemCaption(item, graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
log.Error().Err(err).Msg("Error getting caption")
exceptions.ErrorPage(w, "Error getting caption")
return
}
err = components.EditCaption(item, caption, r).Render(w)
if err != nil {
log.Error().Err(err).Msg("Error rendering caption response")
}
}
module.exports = {
purge: {
enabled: true,
content: [
'./web/**/*.go'
]
},
darkMode: false, // or 'media' or 'class'
theme: {
colors: {
// Configure your color palette here
// See https://coolors.co/ed5c5a-d78f09-252f41-96c0b7-e7ea1f
red: {
light: '#F5A5A3',
DEFAULT: '#ED5C5A',
dark: '#DC1C18',
},
gold: {
light: '#FAD389',
DEFAULT: '#F6AE2D',
dark: '#D78F09',
},
gray: {
paper: '#F2F4F8',
lightest: "#E5E9F0",
lighter: "#B1BDD2",
light: '#7D92B5',
DEFAULT: '#34425B',
dark: '#2E3A4D',
black: '#161C27'
},
opal: {
light: '#CCE0DC',
DEFAULT: '#96C0B7',
dark: '#66A396'
},
yellow: {
light: '#F7F8B4',
DEFAULT: '#EFF16A',
dark: '#E7EA1F'
},
white: {
DEFAULT: '#FFFFFF',
}
},
extend: {
dropShadow: {
'2xl': '0 35px 35px rgba(52, 66, 91, 0.25)',
}
}
},
variants: {
extend: {},
},
plugins: [],
}
---
#########
# TEMP #
#########
"TempLocation" : "temp"
###########
# LOGGING #
###########
"LogLevel": "debug" # debug, info, warn, error, fatal
"LogOutput": "stdout" # stdout, or file path
#################
# ASSET STORAGE #
#################
"AssetStorageType": "s3" # s3, or local
## LOCAL ##
"AssetLocalPath": "fs://../../../Nextcloud/Photos"
## S3 ##
"AssetS3AccessKeyId": "***"
"AssetS3SecretAccessKey": "***"
"AssetS3Region": "us-east-1"
"AssetS3Bucket": "***"
"AssetS3Endpoint": "https://us-east-1.linodeobjects.com"
#################
# GRAPH STORAGE #
#################
"GraphEndpoint": "https://***.us-east-1.aws.cloud.dgraph.io"
"GraphAPIKey": "***"
#################
# SCAFFOLDING #
#################
## Users ##
DefaultUser:
"username": "admin"
"password": "***"
"email": ""
##############
# SECURITY #
##############
"CSRFKey": "***"
...
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"os/signal"
"webster/MemoryLane/cmd"
"webster/MemoryLane/global"
"github.com/rs/zerolog/log"
)
func main() {
ctx := global.GetGlobalContext()
// trap Ctrl+C and call cancel on the context
ctx, cancel := context.WithCancel(ctx)
global.SetGlobalContext(ctx)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
defer func() {
signal.Stop(c)
cancel()
}()
go func() {
select {
case <-c:
fmt.Println("\033[2J")
fmt.Println("***********************")
fmt.Println("* Shut Down Requested *")
fmt.Println("***********************")
log.Info().Msg("*** Shut Down Requested ***")
cancel()
global.Shutdown()
case <-ctx.Done():
}
}()
cmd.Execute()
}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:g_getenv( \"PATH\" ) == \"/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/MacGPG2/bin\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/opt/homebrew/bin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/opt/homebrew/sbin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/usr/local/bin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/usr/bin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/bin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/usr/sbin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/sbin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:looking in \"/usr/local/MacGPG2/bin\" for \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:trying for dir = \"/Users/christopherwebster/Projects/MemoryLane/govips\", name = \"govips\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:canonicalised path = \"/Users/christopherwebster/Projects/MemoryLane\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:VIPS_PREFIX = /opt/homebrew/Cellar/vips/8.12.1"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:VIPS_LIBDIR = /opt/homebrew/Cellar/vips/8.12.1/lib"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:prefix = /opt/homebrew/Cellar/vips/8.12.1"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:libdir = /opt/homebrew/Cellar/vips/8.12.1/lib"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:searching \"/opt/homebrew/Cellar/vips/8.12.1/lib/vips-plugins-8.12\""}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:vips 8.12.1 started with concurrency=1 cache_max_files=0 cache_max_mem=52428800 cache_max=100"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=magick"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=pdf"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=webp"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=tiff"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=heif"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=heif"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=gif"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=jpeg"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=png"}
{"level":"info","time":"2021-12-21T15:13:21-05:00","message":"VIPS:registered image type loader type=svg"}
{"level":"debug","caller":"/Users/christopherwebster/Projects/MemoryLane/graphStore/connect.go:41","time":"2021-12-21T15:13:21-05:00","message":"Initializing Connection to EliasDB ."}
{"level":"fatal","error":"GraphError: Failed to open graph storage (mkdir : no such file or directory)","time":"2021-12-21T15:13:21-05:00","message":"Could not create graph storage"}
//AMD insanity
(function (root, factory) {
//@ts-ignore
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
//@ts-ignore
define([], factory);
} else {
// Browser globals
root.htmx = factory();
}
}(typeof self !== 'undefined' ? self : this, function () {
return (function () {
'use strict';
// Public API
//** @type {import("./htmx").HtmxApi} */
// TODO: list all methods in public API
var htmx = {
onLoad: onLoadHelper,
process: processNode,
on: addEventListenerImpl,
off: removeEventListenerImpl,
trigger : triggerEvent,
ajax : ajaxHelper,
find : find,
findAll : findAll,
closest : closest,
values : function(elt, type){
var inputValues = getInputValues(elt, type || "post");
return inputValues.values;
},
remove : removeElement,
addClass : addClassToElement,
removeClass : removeClassFromElement,
toggleClass : toggleClassOnElement,
takeClass : takeClassForElement,
defineExtension : defineExtension,
removeExtension : removeExtension,
logAll : logAll,
logger : null,
config : {
historyEnabled:true,
historyCacheSize:10,
refreshOnHistoryMiss:false,
defaultSwapStyle:'innerHTML',
defaultSwapDelay:0,
defaultSettleDelay:20,
includeIndicatorStyles:true,
indicatorClass:'htmx-indicator',
requestClass:'htmx-request',
addedClass:'htmx-added',
settlingClass:'htmx-settling',
swappingClass:'htmx-swapping',
allowEval:true,
attributesToSettle:["class", "style", "width", "height"],
withCredentials:false,
timeout:0,
disableSelector: "[hx-disable], [data-hx-disable]",
useTemplateFragments: false,
scrollBehavior: 'smooth',
},
parseInterval:parseInterval,
_:internalEval,
version: "1.7.0"
};
/** @type {import("./htmx").HtmxInternalApi} */
var internalAPI = {
bodyContains: bodyContains,
filterValues: filterValues,
hasAttribute: hasAttribute,
getAttributeValue: getAttributeValue,
getClosestMatch: getClosestMatch,
getExpressionVars: getExpressionVars,
getHeaders: getHeaders,
getInputValues: getInputValues,
getInternalData: getInternalData,
getSwapSpecification: getSwapSpecification,
getTriggerSpecs: getTriggerSpecs,
getTarget: getTarget,
makeFragment: makeFragment,
mergeObjects: mergeObjects,
makeSettleInfo: makeSettleInfo,
oobSwap: oobSwap,
selectAndSwap: selectAndSwap,
settleImmediately: settleImmediately,
shouldCancel: shouldCancel,
triggerEvent: triggerEvent,
triggerErrorEvent: triggerErrorEvent,
withExtensions: withExtensions,
}
var VERBS = ['get', 'post', 'put', 'delete', 'patch'];
var VERB_SELECTOR = VERBS.map(function(verb){
return "[hx-" + verb + "], [data-hx-" + verb + "]"
}).join(", ");
//====================================================================
// Utilities
//====================================================================
function parseInterval(str) {
if (str == undefined) {
return undefined
}
if (str.slice(-2) == "ms") {
return parseFloat(str.slice(0,-2)) || undefined
}
if (str.slice(-1) == "s") {
return (parseFloat(str.slice(0,-1)) * 1000) || undefined
}
return parseFloat(str) || undefined
}
/**
* @param {HTMLElement} elt
* @param {string} name
* @returns {(string | null)}
*/
function getRawAttribute(elt, name) {
return elt.getAttribute && elt.getAttribute(name);
}
// resolve with both hx and data-hx prefixes
function hasAttribute(elt, qualifiedName) {
return elt.hasAttribute && (elt.hasAttribute(qualifiedName) ||
elt.hasAttribute("data-" + qualifiedName));
}
/**
*
* @param {HTMLElement} elt
* @param {string} qualifiedName
* @returns {(string | null)}
*/
function getAttributeValue(elt, qualifiedName) {
return getRawAttribute(elt, qualifiedName) || getRawAttribute(elt, "data-" + qualifiedName);
}
/**
* @param {HTMLElement} elt
* @returns {HTMLElement | null}
*/
function parentElt(elt) {
return elt.parentElement;
}
/**
* @returns {Document}
*/
function getDocument() {
return document;
}
/**
* @param {HTMLElement} elt
* @param {(e:HTMLElement) => boolean} condition
* @returns {HTMLElement | null}
*/
function getClosestMatch(elt, condition) {
if (condition(elt)) {
return elt;
} else if (parentElt(elt)) {
return getClosestMatch(parentElt(elt), condition);
} else {
return null;
}
}
/**
* @param {HTMLElement} elt
* @param {string} attributeName
* @returns {string | null}
*/
function getClosestAttributeValue(elt, attributeName) {
var closestAttr = null;
getClosestMatch(elt, function (e) {
return closestAttr = getAttributeValue(e, attributeName);
});
if (closestAttr !== "unset") {
return closestAttr;
}
}
/**
* @param {HTMLElement} elt
* @param {string} selector
* @returns {boolean}
*/
function matches(elt, selector) {
// @ts-ignore: non-standard properties for browser compatability
// noinspection JSUnresolvedVariable
var matchesFunction = elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector;
return matchesFunction && matchesFunction.call(elt, selector);
}
/**
* @param {string} str
* @returns {string}
*/
function getStartTag(str) {
var tagMatcher = /<([a-z][^\/\0>\x20\t\r\n\f]*)/i
var match = tagMatcher.exec( str );
if (match) {
return match[1].toLowerCase();
} else {
return "";
}
}
/**
*
* @param {string} resp
* @param {number} depth
* @returns {Element}
*/
function parseHTML(resp, depth) {
var parser = new DOMParser();
var responseDoc = parser.parseFromString(resp, "text/html");
/** @type {Element} */
var responseNode = responseDoc.body;
while (depth > 0) {
depth--;
// @ts-ignore
responseNode = responseNode.firstChild;
}
if (responseNode == null) {
// @ts-ignore
responseNode = getDocument().createDocumentFragment();
}
return responseNode;
}
/**
*
* @param {string} resp
* @returns {Element}
*/
function makeFragment(resp) {
if (htmx.config.useTemplateFragments) {
var documentFragment = parseHTML("<body><template>" + resp + "</template></body>", 0);
// @ts-ignore type mismatch between DocumentFragment and Element.
// TODO: Are these close enough for htmx to use interchangably?
return documentFragment.querySelector('template').content;
} else {
var startTag = getStartTag(resp);
switch (startTag) {
case "thead":
case "tbody":
case "tfoot":
case "colgroup":
case "caption":
return parseHTML("<table>" + resp + "</table>", 1);
case "col":
return parseHTML("<table><colgroup>" + resp + "</colgroup></table>", 2);
case "tr":
return parseHTML("<table><tbody>" + resp + "</tbody></table>", 2);
case "td":
case "th":
return parseHTML("<table><tbody><tr>" + resp + "</tr></tbody></table>", 3);
case "script":
return parseHTML("<div>" + resp + "</div>", 1);
default:
return parseHTML(resp, 0);
}
}
}
/**
* @param {Function} func
*/
function maybeCall(func){
if(func) {
func();
}
}
/**
* @param {any} o
* @param {string} type
* @returns
*/
function isType(o, type) {
return Object.prototype.toString.call(o) === "[object " + type + "]";
}
/**
* @param {*} o
* @returns {o is Function}
*/
function isFunction(o) {
return isType(o, "Function");
}
/**
* @param {*} o
* @returns {o is Object}
*/
function isRawObject(o) {
return isType(o, "Object");
}
/**
* getInternalData retrieves "private" data stored by htmx within an element
* @param {HTMLElement} elt
* @returns {*}
*/
function getInternalData(elt) {
var dataProp = 'htmx-internal-data';
var data = elt[dataProp];
if (!data) {
data = elt[dataProp] = {};
}
return data;
}
/**
* toArray converts an ArrayLike object into a real array.
* @param {ArrayLike} arr
* @returns {any[]}
*/
function toArray(arr) {
var returnArr = [];
if (arr) {
for (var i = 0; i < arr.length; i++) {
returnArr.push(arr[i]);
}
}
return returnArr
}
function forEach(arr, func) {
if (arr) {
for (var i = 0; i < arr.length; i++) {
func(arr[i]);
}
}
}
function isScrolledIntoView(el) {
var rect = el.getBoundingClientRect();
var elemTop = rect.top;
var elemBottom = rect.bottom;
return elemTop < window.innerHeight && elemBottom >= 0;
}
function bodyContains(elt) {
return getDocument().body.contains(elt);
}
function splitOnWhitespace(trigger) {
return trigger.trim().split(/\s+/);
}
/**
* mergeObjects takes all of the keys from
* obj2 and duplicates them into obj1
* @param {Object} obj1
* @param {Object} obj2
* @returns {Object}
*/
function mergeObjects(obj1, obj2) {
for (var key in obj2) {
if (obj2.hasOwnProperty(key)) {
obj1[key] = obj2[key];
}
}
return obj1;
}
function parseJSON(jString) {
try {
return JSON.parse(jString);
} catch(error) {
logError(error);
return null;
}
}
//==========================================================================================
// public API
//==========================================================================================
function internalEval(str){
return maybeEval(getDocument().body, function () {
return eval(str);
});
}
function onLoadHelper(callback) {
var value = htmx.on("htmx:load", function(evt) {
callback(evt.detail.elt);
});
return value;
}
function logAll(){
htmx.logger = function(elt, event, data) {
if(console) {
console.log(event, elt, data);
}
}
}
function find(eltOrSelector, selector) {
if (selector) {
return eltOrSelector.querySelector(selector);
} else {
return find(getDocument(), eltOrSelector);
}
}
function findAll(eltOrSelector, selector) {
if (selector) {
return eltOrSelector.querySelectorAll(selector);
} else {
return findAll(getDocument(), eltOrSelector);
}
}
function removeElement(elt, delay) {
elt = resolveTarget(elt);
if (delay) {
setTimeout(function(){removeElement(elt);}, delay)
} else {
elt.parentElement.removeChild(elt);
}
}
function addClassToElement(elt, clazz, delay) {
elt = resolveTarget(elt);
if (delay) {
setTimeout(function(){addClassToElement(elt, clazz);}, delay)
} else {
elt.classList && elt.classList.add(clazz);
}
}
function removeClassFromElement(elt, clazz, delay) {
elt = resolveTarget(elt);
if (delay) {
setTimeout(function(){removeClassFromElement(elt, clazz);}, delay)
} else {
if (elt.classList) {
elt.classList.remove(clazz);
// if there are no classes left, remove the class attribute
if (elt.classList.length === 0) {
elt.removeAttribute("class");
}
}
}
}
function toggleClassOnElement(elt, clazz) {
elt = resolveTarget(elt);
elt.classList.toggle(clazz);
}
function takeClassForElement(elt, clazz) {
elt = resolveTarget(elt);
forEach(elt.parentElement.children, function(child){
removeClassFromElement(child, clazz);
})
addClassToElement(elt, clazz);
}
function closest(elt, selector) {
elt = resolveTarget(elt);
if (elt.closest) {
return elt.closest(selector);
} else {
do{
if (elt == null || matches(elt, selector)){
return elt;
}
}
while (elt = elt && parentElt(elt));
}
}
function querySelectorAllExt(elt, selector) {
if (selector.indexOf("closest ") === 0) {
return [closest(elt, selector.substr(8))];
} else if (selector.indexOf("find ") === 0) {
return [find(elt, selector.substr(5))];
} else if (selector === 'document') {
return [document];
} else if (selector === 'window') {
return [window];
} else {
return getDocument().querySelectorAll(selector);
}
}
function querySelectorExt(eltOrSelector, selector) {
if (selector) {
return querySelectorAllExt(eltOrSelector, selector)[0];
} else {
return querySelectorAllExt(getDocument().body, eltOrSelector)[0];
}
}
function resolveTarget(arg2) {
if (isType(arg2, 'String')) {
return find(arg2);
} else {
return arg2;
}
}
function processEventArgs(arg1, arg2, arg3) {
if (isFunction(arg2)) {
return {
target: getDocument().body,
event: arg1,
listener: arg2
}
} else {
return {
target: resolveTarget(arg1),
event: arg2,
listener: arg3
}
}
}
function addEventListenerImpl(arg1, arg2, arg3) {
ready(function(){
var eventArgs = processEventArgs(arg1, arg2, arg3);
eventArgs.target.addEventListener(eventArgs.event, eventArgs.listener);
})
var b = isFunction(arg2);
return b ? arg2 : arg3;
}
function removeEventListenerImpl(arg1, arg2, arg3) {
ready(function(){
var eventArgs = processEventArgs(arg1, arg2, arg3);
eventArgs.target.removeEventListener(eventArgs.event, eventArgs.listener);
})
return isFunction(arg2) ? arg2 : arg3;
}
//====================================================================
// Node processing
//====================================================================
function getTarget(elt) {
var explicitTarget = getClosestMatch(elt, function(e){return getAttributeValue(e,"hx-target") !== null});
if (explicitTarget) {
var targetStr = getAttributeValue(explicitTarget, "hx-target");
if (targetStr === "this") {
return explicitTarget;
} else {
return querySelectorExt(elt, targetStr)
}
} else {
var data = getInternalData(elt);
if (data.boosted) {
return getDocument().body;
} else {
return elt;
}
}
}
function shouldSettleAttribute(name) {
var attributesToSettle = htmx.config.attributesToSettle;
for (var i = 0; i < attributesToSettle.length; i++) {
if (name === attributesToSettle[i]) {
return true;
}
}
return false;
}
function cloneAttributes(mergeTo, mergeFrom) {
forEach(mergeTo.attributes, function (attr) {
if (!mergeFrom.hasAttribute(attr.name) && shouldSettleAttribute(attr.name)) {
mergeTo.removeAttribute(attr.name)
}
});
forEach(mergeFrom.attributes, function (attr) {
if (shouldSettleAttribute(attr.name)) {
mergeTo.setAttribute(attr.name, attr.value);
}
});
}
function isInlineSwap(swapStyle, target) {
var extensions = getExtensions(target);
for (var i = 0; i < extensions.length; i++) {
var extension = extensions[i];
try {
if (extension.isInlineSwap(swapStyle)) {
return true;
}
} catch(e) {
logError(e);
}
}
return swapStyle === "outerHTML";
}
/**
*
* @param {string} oobValue
* @param {HTMLElement} oobElement
* @param {*} settleInfo
* @returns
*/
function oobSwap(oobValue, oobElement, settleInfo) {
var selector = "#" + oobElement.id;
var swapStyle = "outerHTML";
if (oobValue === "true") {
// do nothing
} else if (oobValue.indexOf(":") > 0) {
swapStyle = oobValue.substr(0, oobValue.indexOf(":"));
selector = oobValue.substr(oobValue.indexOf(":") + 1, oobValue.length);
} else {
swapStyle = oobValue;
}
var targets = getDocument().querySelectorAll(selector);
if (targets) {
forEach(
targets,
function (target) {
var fragment;
var oobElementClone = oobElement.cloneNode(true);
fragment = getDocument().createDocumentFragment();
fragment.appendChild(oobElementClone);
if (!isInlineSwap(swapStyle, target)) {
fragment = oobElementClone; // if this is not an inline swap, we use the content of the node, not the node itself
}
swap(swapStyle, target, target, fragment, settleInfo);
}
);
oobElement.parentNode.removeChild(oobElement);
} else {
oobElement.parentNode.removeChild(oobElement);
triggerErrorEvent(getDocument().body, "htmx:oobErrorNoTarget", {content: oobElement})
}
return oobValue;
}
function handleOutOfBandSwaps(fragment, settleInfo) {
forEach(findAll(fragment, '[hx-swap-oob], [data-hx-swap-oob]'), function (oobElement) {
var oobValue = getAttributeValue(oobElement, "hx-swap-oob");
if (oobValue != null) {
oobSwap(oobValue, oobElement, settleInfo);
}
});
}
function handlePreservedElements(fragment) {
forEach(findAll(fragment, '[hx-preserve], [data-hx-preserve]'), function (preservedElt) {
var id = getAttributeValue(preservedElt, "id");
var oldElt = getDocument().getElementById(id);
if (oldElt != null) {
preservedElt.parentNode.replaceChild(oldElt, preservedElt);
}
});
}
function handleAttributes(parentNode, fragment, settleInfo) {
forEach(fragment.querySelectorAll("[id]"), function (newNode) {
if (newNode.id && newNode.id.length > 0) {
var oldNode = parentNode.querySelector(newNode.tagName + "[id='" + newNode.id + "']");
if (oldNode && oldNode !== parentNode) {
var newAttributes = newNode.cloneNode();
cloneAttributes(newNode, oldNode);
settleInfo.tasks.push(function () {
cloneAttributes(newNode, newAttributes);
});
}
}
});
}
function makeAjaxLoadTask(child) {
return function () {
removeClassFromElement(child, htmx.config.addedClass);
processNode(child);
processScripts(child);
processFocus(child)
triggerEvent(child, 'htmx:load');
};
}
function processFocus(child) {
var autofocus = "[autofocus]";
var autoFocusedElt = matches(child, autofocus) ? child : child.querySelector(autofocus)
if (autoFocusedElt != null) {
autoFocusedElt.focus();
}
}
function insertNodesBefore(parentNode, insertBefore, fragment, settleInfo) {
handleAttributes(parentNode, fragment, settleInfo);
while(fragment.childNodes.length > 0){
var child = fragment.firstChild;
addClassToElement(child, htmx.config.addedClass);
parentNode.insertBefore(child, insertBefore);
if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
settleInfo.tasks.push(makeAjaxLoadTask(child));
}
}
}
function cleanUpElement(element) {
var internalData = getInternalData(element);
triggerEvent(element, "htmx:beforeCleanupElement")
if (internalData.listenerInfos) {
forEach(internalData.listenerInfos, function(info) {
if (element !== info.on) {
info.on.removeEventListener(info.trigger, info.listener);
}
});
}
if (element.children) { // IE
forEach(element.children, function(child) { cleanUpElement(child) });
}
}
function swapOuterHTML(target, fragment, settleInfo) {
if (target.tagName === "BODY") {
return swapInnerHTML(target, fragment, settleInfo);
} else {
// @type {HTMLElement}
var newElt
var eltBeforeNewContent = target.previousSibling;
insertNodesBefore(parentElt(target), target, fragment, settleInfo);
if (eltBeforeNewContent == null) {
newElt = parentElt(target).firstChild;
} else {
newElt = eltBeforeNewContent.nextSibling;
}
getInternalData(target).replacedWith = newElt; // tuck away so we can fire events on it later
settleInfo.elts = [] // clear existing elements
while(newElt && newElt !== target) {
if (newElt.nodeType === Node.ELEMENT_NODE) {
settleInfo.elts.push(newElt);
}
newElt = newElt.nextElementSibling;
}
cleanUpElement(target);
parentElt(target).removeChild(target);
}
}
function swapAfterBegin(target, fragment, settleInfo) {
return insertNodesBefore(target, target.firstChild, fragment, settleInfo);
}
function swapBeforeBegin(target, fragment, settleInfo) {
return insertNodesBefore(parentElt(target), target, fragment, settleInfo);
}
function swapBeforeEnd(target, fragment, settleInfo) {
return insertNodesBefore(target, null, fragment, settleInfo);
}
function swapAfterEnd(target, fragment, settleInfo) {
return insertNodesBefore(parentElt(target), target.nextSibling, fragment, settleInfo);
}
function swapInnerHTML(target, fragment, settleInfo) {
var firstChild = target.firstChild;
insertNodesBefore(target, firstChild, fragment, settleInfo);
if (firstChild) {
while (firstChild.nextSibling) {
cleanUpElement(firstChild.nextSibling)
target.removeChild(firstChild.nextSibling);
}
cleanUpElement(firstChild)
target.removeChild(firstChild);
}
}
function maybeSelectFromResponse(elt, fragment) {
var selector = getClosestAttributeValue(elt, "hx-select");
if (selector) {
var newFragment = getDocument().createDocumentFragment();
forEach(fragment.querySelectorAll(selector), function (node) {
newFragment.appendChild(node);
});
fragment = newFragment;
}
return fragment;
}
function swap(swapStyle, elt, target, fragment, settleInfo) {
switch (swapStyle) {
case "none":
return;
case "outerHTML":
swapOuterHTML(target, fragment, settleInfo);
return;
case "afterbegin":
swapAfterBegin(target, fragment, settleInfo);
return;
case "beforebegin":
swapBeforeBegin(target, fragment, settleInfo);
return;
case "beforeend":
swapBeforeEnd(target, fragment, settleInfo);
return;
case "afterend":
swapAfterEnd(target, fragment, settleInfo);
return;
default:
var extensions = getExtensions(elt);
for (var i = 0; i < extensions.length; i++) {
var ext = extensions[i];
try {
var newElements = ext.handleSwap(swapStyle, target, fragment, settleInfo);
if (newElements) {
if (typeof newElements.length !== 'undefined') {
// if handleSwap returns an array (like) of elements, we handle them
for (var j = 0; j < newElements.length; j++) {
var child = newElements[j];
if (child.nodeType !== Node.TEXT_NODE && child.nodeType !== Node.COMMENT_NODE) {
settleInfo.tasks.push(makeAjaxLoadTask(child));
}
}
}
return;
}
} catch (e) {
logError(e);
}
}
swapInnerHTML(target, fragment, settleInfo);
}
}
function findTitle(content) {
if (content.indexOf('<title') > -1) {
var contentWithSvgsRemoved = content.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, '');
var result = contentWithSvgsRemoved.match(/<title(\s[^>]*>|>)([\s\S]*?)<\/title>/im);
if (result) {
return result[2];
}
}
}
function selectAndSwap(swapStyle, target, elt, responseText, settleInfo) {
var title = findTitle(responseText);
if(title) {
var titleElt = find("title");
if(titleElt) {
titleElt.innerHTML = title;
} else {
window.document.title = title;
}
}
var fragment = makeFragment(responseText);
if (fragment) {
handleOutOfBandSwaps(fragment, settleInfo);
fragment = maybeSelectFromResponse(elt, fragment);
handlePreservedElements(fragment);
return swap(swapStyle, elt, target, fragment, settleInfo);
}
}
function handleTrigger(xhr, header, elt) {
var triggerBody = xhr.getResponseHeader(header);
if (triggerBody.indexOf("{") === 0) {
var triggers = parseJSON(triggerBody);
for (var eventName in triggers) {
if (triggers.hasOwnProperty(eventName)) {
var detail = triggers[eventName];
if (!isRawObject(detail)) {
detail = {"value": detail}
}
triggerEvent(elt, eventName, detail);
}
}
} else {
triggerEvent(elt, triggerBody, []);
}
}
var WHITESPACE = /\s/;
var WHITESPACE_OR_COMMA = /[\s,]/;
var SYMBOL_START = /[_$a-zA-Z]/;
var SYMBOL_CONT = /[_$a-zA-Z0-9]/;
var STRINGISH_START = ['"', "'", "/"];
var NOT_WHITESPACE = /[^\s]/;
function tokenizeString(str) {
var tokens = [];
var position = 0;
while (position < str.length) {
if(SYMBOL_START.exec(str.charAt(position))) {
var startPosition = position;
while (SYMBOL_CONT.exec(str.charAt(position + 1))) {
position++;
}
tokens.push(str.substr(startPosition, position - startPosition + 1));
} else if (STRINGISH_START.indexOf(str.charAt(position)) !== -1) {
var startChar = str.charAt(position);
var startPosition = position;
position++;
while (position < str.length && str.charAt(position) !== startChar ) {
if (str.charAt(position) === "\\") {
position++;
}
position++;
}
tokens.push(str.substr(startPosition, position - startPosition + 1));
} else {
var symbol = str.charAt(position);
tokens.push(symbol);
}
position++;
}
return tokens;
}
function isPossibleRelativeReference(token, last, paramName) {
return SYMBOL_START.exec(token.charAt(0)) &&
token !== "true" &&
token !== "false" &&
token !== "this" &&
token !== paramName &&
last !== ".";
}
function maybeGenerateConditional(elt, tokens, paramName) {
if (tokens[0] === '[') {
tokens.shift();
var bracketCount = 1;
var conditionalSource = " return (function(" + paramName + "){ return (";
var last = null;
while (tokens.length > 0) {
var token = tokens[0];
if (token === "]") {
bracketCount--;
if (bracketCount === 0) {
if (last === null) {
conditionalSource = conditionalSource + "true";
}
tokens.shift();
conditionalSource += ")})";
try {
var conditionFunction = maybeEval(elt,function () {
return Function(conditionalSource)();
},
function(){return true})
conditionFunction.source = conditionalSource;
return conditionFunction;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:syntax:error", {error:e, source:conditionalSource})
return null;
}
}
} else if (token === "[") {
bracketCount++;
}
if (isPossibleRelativeReference(token, last, paramName)) {
conditionalSource += "((" + paramName + "." + token + ") ? (" + paramName + "." + token + ") : (window." + token + "))";
} else {
conditionalSource = conditionalSource + token;
}
last = tokens.shift();
}
}
}
function consumeUntil(tokens, match) {
var result = "";
while (tokens.length > 0 && !tokens[0].match(match)) {
result += tokens.shift();
}
return result;
}
var INPUT_SELECTOR = 'input, textarea, select';
/**
* @param {HTMLElement} elt
* @returns {import("./htmx").HtmxTriggerSpecification[]}
*/
function getTriggerSpecs(elt) {
var explicitTrigger = getAttributeValue(elt, 'hx-trigger');
var triggerSpecs = [];
if (explicitTrigger) {
var tokens = tokenizeString(explicitTrigger);
do {
consumeUntil(tokens, NOT_WHITESPACE);
var initialLength = tokens.length;
var trigger = consumeUntil(tokens, /[,\[\s]/);
if (trigger !== "") {
if (trigger === "every") {
var every = {trigger: 'every'};
consumeUntil(tokens, NOT_WHITESPACE);
every.pollInterval = parseInterval(consumeUntil(tokens, /[,\[\s]/));
consumeUntil(tokens, NOT_WHITESPACE);
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
if (eventFilter) {
every.eventFilter = eventFilter;
}
triggerSpecs.push(every);
} else {
var triggerSpec = {trigger: trigger};
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
if (eventFilter) {
triggerSpec.eventFilter = eventFilter;
}
while (tokens.length > 0 && tokens[0] !== ",") {
consumeUntil(tokens, NOT_WHITESPACE)
var token = tokens.shift();
if (token === "changed") {
triggerSpec.changed = true;
} else if (token === "once") {
triggerSpec.once = true;
} else if (token === "consume") {
triggerSpec.consume = true;
} else if (token === "delay" && tokens[0] === ":") {
tokens.shift();
triggerSpec.delay = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
} else if (token === "from" && tokens[0] === ":") {
tokens.shift();
let from_arg = consumeUntil(tokens, WHITESPACE_OR_COMMA);
if (from_arg === "closest" || from_arg === "find") {
tokens.shift();
from_arg +=
" " +
consumeUntil(
tokens,
WHITESPACE_OR_COMMA
);
}
triggerSpec.from = from_arg;
} else if (token === "target" && tokens[0] === ":") {
tokens.shift();
triggerSpec.target = consumeUntil(tokens, WHITESPACE_OR_COMMA);
} else if (token === "throttle" && tokens[0] === ":") {
tokens.shift();
triggerSpec.throttle = parseInterval(consumeUntil(tokens, WHITESPACE_OR_COMMA));
} else if (token === "queue" && tokens[0] === ":") {
tokens.shift();
triggerSpec.queue = consumeUntil(tokens, WHITESPACE_OR_COMMA);
} else if ((token === "root" || token === "threshold") && tokens[0] === ":") {
tokens.shift();
triggerSpec[token] = consumeUntil(tokens, WHITESPACE_OR_COMMA);
} else {
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
}
}
triggerSpecs.push(triggerSpec);
}
}
if (tokens.length === initialLength) {
triggerErrorEvent(elt, "htmx:syntax:error", {token:tokens.shift()});
}
consumeUntil(tokens, NOT_WHITESPACE);
} while (tokens[0] === "," && tokens.shift())
}
if (triggerSpecs.length > 0) {
return triggerSpecs;
} else if (matches(elt, 'form')) {
return [{trigger: 'submit'}];
} else if (matches(elt, INPUT_SELECTOR)) {
return [{trigger: 'change'}];
} else {
return [{trigger: 'click'}];
}
}
function cancelPolling(elt) {
getInternalData(elt).cancelled = true;
}
function processPolling(elt, verb, path, spec) {
var nodeData = getInternalData(elt);
nodeData.timeout = setTimeout(function () {
if (bodyContains(elt) && nodeData.cancelled !== true) {
if (!maybeFilterEvent(spec, makeEvent('hx:poll:trigger', {triggerSpec:spec}))) {
issueAjaxRequest(verb, path, elt);
}
processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb), spec);
}
}, spec.pollInterval);
}
function isLocalLink(elt) {
return location.hostname === elt.hostname &&
getRawAttribute(elt,'href') &&
getRawAttribute(elt,'href').indexOf("#") !== 0;
}
function boostElement(elt, nodeData, triggerSpecs) {
if ((elt.tagName === "A" && isLocalLink(elt) && elt.target === "") || elt.tagName === "FORM") {
nodeData.boosted = true;
var verb, path;
if (elt.tagName === "A") {
verb = "get";
path = getRawAttribute(elt, 'href');
nodeData.pushURL = true;
} else {
var rawAttribute = getRawAttribute(elt, "method");
verb = rawAttribute ? rawAttribute.toLowerCase() : "get";
if (verb === "get") {
nodeData.pushURL = true;
}
path = getRawAttribute(elt, 'action');
}
triggerSpecs.forEach(function(triggerSpec) {
addEventListener(elt, verb, path, nodeData, triggerSpec, true);
});
}
}
/**
*
* @param {Event} evt
* @param {HTMLElement} elt
* @returns
*/
function shouldCancel(evt, elt) {
if (evt.type === "submit" || evt.type === "click") {
if (elt.tagName === "FORM") {
return true;
}
if (matches(elt, 'input[type="submit"], button') && closest(elt, 'form') !== null) {
return true;
}
if (elt.tagName === "A" && elt.href &&
(elt.getAttribute('href') === '#' || elt.getAttribute('href').indexOf("#") !== 0)) {
return true;
}
}
return false;
}
function ignoreBoostedAnchorCtrlClick(elt, evt) {
return getInternalData(elt).boosted && elt.tagName === "A" && evt.type === "click" && (evt.ctrlKey || evt.metaKey);
}
function maybeFilterEvent(triggerSpec, evt) {
var eventFilter = triggerSpec.eventFilter;
if(eventFilter){
try {
return eventFilter(evt) !== true;
} catch(e) {
triggerErrorEvent(getDocument().body, "htmx:eventFilter:error", {error: e, source:eventFilter.source});
return true;
}
}
return false;
}
function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) {
var eltsToListenOn;
if (triggerSpec.from) {
eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from);
} else {
eltsToListenOn = [elt];
}
forEach(eltsToListenOn, function (eltToListenOn) {
var eventListener = function (evt) {
if (!bodyContains(elt)) {
eltToListenOn.removeEventListener(triggerSpec.trigger, eventListener);
return;
}
if (ignoreBoostedAnchorCtrlClick(elt, evt)) {
return;
}
if (explicitCancel || shouldCancel(evt, elt)) {
evt.preventDefault();
}
if (maybeFilterEvent(triggerSpec, evt)) {
return;
}
var eventData = getInternalData(evt);
eventData.triggerSpec = triggerSpec;
if (eventData.handledFor == null) {
eventData.handledFor = [];
}
var elementData = getInternalData(elt);
if (eventData.handledFor.indexOf(elt) < 0) {
eventData.handledFor.push(elt);
if (triggerSpec.consume) {
evt.stopPropagation();
}
if (triggerSpec.target && evt.target) {
if (!matches(evt.target, triggerSpec.target)) {
return;
}
}
if (triggerSpec.once) {
if (elementData.triggeredOnce) {
return;
} else {
elementData.triggeredOnce = true;
}
}
if (triggerSpec.changed) {
if (elementData.lastValue === elt.value) {
return;
} else {
elementData.lastValue = elt.value;
}
}
if (elementData.delayed) {
clearTimeout(elementData.delayed);
}
if (elementData.throttle) {
return;
}
if (triggerSpec.throttle) {
if (!elementData.throttle) {
issueAjaxRequest(verb, path, elt, evt);
elementData.throttle = setTimeout(function () {
elementData.throttle = null;
}, triggerSpec.throttle);
}
} else if (triggerSpec.delay) {
elementData.delayed = setTimeout(function () {
issueAjaxRequest(verb, path, elt, evt);
}, triggerSpec.delay);
} else {
issueAjaxRequest(verb, path, elt, evt);
}
}
};
if (nodeData.listenerInfos == null) {
nodeData.listenerInfos = [];
}
nodeData.listenerInfos.push({
trigger: triggerSpec.trigger,
listener: eventListener,
on: eltToListenOn
})
eltToListenOn.addEventListener(triggerSpec.trigger, eventListener);
})
}
var windowIsScrolling = false // used by initScrollHandler
var scrollHandler = null;
function initScrollHandler() {
if (!scrollHandler) {
scrollHandler = function() {
windowIsScrolling = true
};
window.addEventListener("scroll", scrollHandler)
setInterval(function() {
if (windowIsScrolling) {
windowIsScrolling = false;
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
maybeReveal(elt);
})
}
}, 200);
}
}
function maybeReveal(elt) {
if (!hasAttribute(elt,'data-hx-revealed') && isScrolledIntoView(elt)) {
elt.setAttribute('data-hx-revealed', 'true');
var nodeData = getInternalData(elt);
if (nodeData.initialized) {
issueAjaxRequest(nodeData.verb, nodeData.path, elt);
} else {
// if the node isn't initialized, wait for it before triggering the request
elt.addEventListener("htmx:afterProcessNode",
function () {
issueAjaxRequest(nodeData.verb, nodeData.path, elt);
}, {once: true});
}
}
}
//====================================================================
function loadImmediately(elt, verb, path, nodeData, delay) {
var load = function(){
if (!nodeData.loaded) {
nodeData.loaded = true;
issueAjaxRequest(verb, path, elt);
}
}
if (delay) {
setTimeout(load, delay);
} else {
load();
}
}
function processVerbs(elt, nodeData, triggerSpecs) {
var explicitAction = false;
forEach(VERBS, function (verb) {
if (hasAttribute(elt,'hx-' + verb)) {
var path = getAttributeValue(elt, 'hx-' + verb);
explicitAction = true;
nodeData.path = path;
nodeData.verb = verb;
triggerSpecs.forEach(function(triggerSpec) {
if (triggerSpec.trigger === "revealed") {
initScrollHandler();
maybeReveal(elt);
} else if (triggerSpec.trigger === "intersect") {
var observerOptions = {};
if (triggerSpec.root) {
observerOptions.root = querySelectorExt(elt, triggerSpec.root)
}
if (triggerSpec.threshold) {
observerOptions.threshold = parseFloat(triggerSpec.threshold);
}
var observer = new IntersectionObserver(function (entries) {
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (entry.isIntersecting) {
triggerEvent(elt, "intersect");
break;
}
}
}, observerOptions);
observer.observe(elt);
addEventListener(elt, verb, path, nodeData, triggerSpec);
} else if (triggerSpec.trigger === "load") {
loadImmediately(elt, verb, path, nodeData, triggerSpec.delay);
} else if (triggerSpec.pollInterval) {
nodeData.polling = true;
processPolling(elt, verb, path, triggerSpec);
} else {
addEventListener(elt, verb, path, nodeData, triggerSpec);
}
});
}
});
return explicitAction;
}
function evalScript(script) {
if (script.type === "text/javascript" || script.type === "") {
var newScript = getDocument().createElement("script");
forEach(script.attributes, function (attr) {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = script.textContent;
newScript.async = false;
var parent = script.parentElement;
try {
parent.insertBefore(newScript, script);
} catch (e) {
logError(e);
} finally {
parent.removeChild(script);
}
}
}
function processScripts(elt) {
if (matches(elt, "script")) {
evalScript(elt);
}
forEach(findAll(elt, "script"), function (script) {
evalScript(script);
});
}
function isBoosted() {
return document.querySelector("[hx-boost], [data-hx-boost]");
}
function findElementsToProcess(elt) {
if (elt.querySelectorAll) {
var boostedElts = isBoosted() ? ", a, form" : "";
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-ext], [data-hx-ext]");
return results;
} else {
return [];
}
}
function initButtonTracking(form){
var maybeSetLastButtonClicked = function(evt){
if (matches(evt.target, "button, input[type='submit']")) {
var internalData = getInternalData(form);
internalData.lastButtonClicked = evt.target;
}
};
// need to handle both click and focus in:
// focusin - in case someone tabs in to a button and hits the space bar
// click - on OSX buttons do not focus on click see https://bugs.webkit.org/show_bug.cgi?id=13724
form.addEventListener('click', maybeSetLastButtonClicked)
form.addEventListener('focusin', maybeSetLastButtonClicked)
form.addEventListener('focusout', function(evt){
var internalData = getInternalData(form);
internalData.lastButtonClicked = null;
})
}
function initNode(elt) {
if (elt.closest && elt.closest(htmx.config.disableSelector)) {
return;
}
var nodeData = getInternalData(elt);
if (!nodeData.initialized) {
nodeData.initialized = true;
triggerEvent(elt, "htmx:beforeProcessNode")
if (elt.value) {
nodeData.lastValue = elt.value;
}
var triggerSpecs = getTriggerSpecs(elt);
var explicitAction = processVerbs(elt, nodeData, triggerSpecs);
if (!explicitAction && getClosestAttributeValue(elt, "hx-boost") === "true") {
boostElement(elt, nodeData, triggerSpecs);
}
if (elt.tagName === "FORM") {
initButtonTracking(elt);
}
triggerEvent(elt, "htmx:afterProcessNode");
}
}
function processNode(elt) {
elt = resolveTarget(elt);
initNode(elt);
forEach(findElementsToProcess(elt), function(child) { initNode(child) });
}
//====================================================================
// Event/Log Support
//====================================================================
function kebabEventName(str) {
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
}
function makeEvent(eventName, detail) {
var evt;
if (window.CustomEvent && typeof window.CustomEvent === 'function') {
evt = new CustomEvent(eventName, {bubbles: true, cancelable: true, detail: detail});
} else {
evt = getDocument().createEvent('CustomEvent');
evt.initCustomEvent(eventName, true, true, detail);
}
return evt;
}
function triggerErrorEvent(elt, eventName, detail) {
triggerEvent(elt, eventName, mergeObjects({error:eventName}, detail));
}
function ignoreEventForLogging(eventName) {
return eventName === "htmx:afterProcessNode"
}
/**
* `withExtensions` locates all active extensions for a provided element, then
* executes the provided function using each of the active extensions. It should
* be called internally at every extendable execution point in htmx.
*
* @param {HTMLElement} elt
* @param {(extension:import("./htmx").HtmxExtension) => void} toDo
* @returns void
*/
function withExtensions(elt, toDo) {
forEach(getExtensions(elt), function(extension){
try {
toDo(extension);
} catch (e) {
logError(e);
}
});
}
function logError(msg) {
if(console.error) {
console.error(msg);
} else if (console.log) {
console.log("ERROR: ", msg);
}
}
function triggerEvent(elt, eventName, detail) {
elt = resolveTarget(elt);
if (detail == null) {
detail = {};
}
detail["elt"] = elt;
var event = makeEvent(eventName, detail);
if (htmx.logger && !ignoreEventForLogging(eventName)) {
htmx.logger(elt, eventName, detail);
}
if (detail.error) {
logError(detail.error);
triggerEvent(elt, "htmx:error", {errorInfo:detail})
}
var eventResult = elt.dispatchEvent(event);
var kebabName = kebabEventName(eventName);
if (eventResult && kebabName !== eventName) {
var kebabedEvent = makeEvent(kebabName, event.detail);
eventResult = eventResult && elt.dispatchEvent(kebabedEvent)
}
withExtensions(elt, function (extension) {
eventResult = eventResult && (extension.onEvent(eventName, event) !== false)
});
return eventResult;
}
//====================================================================
// History Support
//====================================================================
var currentPathForHistory = location.pathname+location.search;
function getHistoryElement() {
var historyElt = getDocument().querySelector('[hx-history-elt],[data-hx-history-elt]');
return historyElt || getDocument().body;
}
function saveToHistoryCache(url, content, title, scroll) {
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
for (var i = 0; i < historyCache.length; i++) {
if (historyCache[i].url === url) {
historyCache.splice(i, 1);
break;
}
}
historyCache.push({url:url, content: content, title:title, scroll:scroll})
while (historyCache.length > htmx.config.historyCacheSize) {
historyCache.shift();
}
while(historyCache.length > 0){
try {
localStorage.setItem("htmx-history-cache", JSON.stringify(historyCache));
break;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:historyCacheError", {cause:e, cache: historyCache})
historyCache.shift(); // shrink the cache and retry
}
}
}
function getCachedHistory(url) {
var historyCache = parseJSON(localStorage.getItem("htmx-history-cache")) || [];
for (var i = 0; i < historyCache.length; i++) {
if (historyCache[i].url === url) {
return historyCache[i];
}
}
return null;
}
function cleanInnerHtmlForHistory(elt) {
var className = htmx.config.requestClass;
var clone = elt.cloneNode(true);
forEach(findAll(clone, "." + className), function(child){
removeClassFromElement(child, className);
});
return clone.innerHTML;
}
function saveHistory() {
var elt = getHistoryElement();
var path = currentPathForHistory || location.pathname+location.search;
triggerEvent(getDocument().body, "htmx:beforeHistorySave", {path:path, historyElt:elt});
if(htmx.config.historyEnabled) history.replaceState({htmx:true}, getDocument().title, window.location.href);
saveToHistoryCache(path, cleanInnerHtmlForHistory(elt), getDocument().title, window.scrollY);
}
function pushUrlIntoHistory(path) {
if(htmx.config.historyEnabled) history.pushState({htmx:true}, "", path);
currentPathForHistory = path;
}
function settleImmediately(tasks) {
forEach(tasks, function (task) {
task.call();
});
}
function loadHistoryFromServer(path) {
var request = new XMLHttpRequest();
var details = {path: path, xhr:request};
triggerEvent(getDocument().body, "htmx:historyCacheMiss", details);
request.open('GET', path, true);
request.setRequestHeader("HX-History-Restore-Request", "true");
request.onload = function () {
if (this.status >= 200 && this.status < 400) {
triggerEvent(getDocument().body, "htmx:historyCacheMissLoad", details);
var fragment = makeFragment(this.response);
// @ts-ignore
fragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment;
var historyElement = getHistoryElement();
var settleInfo = makeSettleInfo(historyElement);
// @ts-ignore
swapInnerHTML(historyElement, fragment, settleInfo)
settleImmediately(settleInfo.tasks);
currentPathForHistory = path;
triggerEvent(getDocument().body, "htmx:historyRestore", {path:path});
} else {
triggerErrorEvent(getDocument().body, "htmx:historyCacheMissLoadError", details);
}
};
request.send();
}
function restoreHistory(path) {
saveHistory();
path = path || location.pathname+location.search;
var cached = getCachedHistory(path);
if (cached) {
var fragment = makeFragment(cached.content);
var historyElement = getHistoryElement();
var settleInfo = makeSettleInfo(historyElement);
swapInnerHTML(historyElement, fragment, settleInfo)
settleImmediately(settleInfo.tasks);
document.title = cached.title;
window.scrollTo(0, cached.scroll);
currentPathForHistory = path;
triggerEvent(getDocument().body, "htmx:historyRestore", {path:path});
} else {
if (htmx.config.refreshOnHistoryMiss) {
// @ts-ignore: optional parameter in reload() function throws error
window.location.reload(true);
} else {
loadHistoryFromServer(path);
}
}
}
function shouldPush(elt) {
var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
return (pushUrl && pushUrl !== "false") ||
(getInternalData(elt).boosted && getInternalData(elt).pushURL);
}
function getPushUrl(elt) {
var pushUrl = getClosestAttributeValue(elt, "hx-push-url");
return (pushUrl === "true" || pushUrl === "false") ? null : pushUrl;
}
function addRequestIndicatorClasses(elt) {
var indicator = getClosestAttributeValue(elt, 'hx-indicator');
if (indicator) {
var indicators = querySelectorAllExt(elt, indicator);
} else {
indicators = [elt];
}
forEach(indicators, function (ic) {
ic.classList["add"].call(ic.classList, htmx.config.requestClass);
});
return indicators;
}
function removeRequestIndicatorClasses(indicators) {
forEach(indicators, function (ic) {
ic.classList["remove"].call(ic.classList, htmx.config.requestClass);
});
}
//====================================================================
// Input Value Processing
//====================================================================
function haveSeenNode(processed, elt) {
for (var i = 0; i < processed.length; i++) {
var node = processed[i];
if (node.isSameNode(elt)) {
return true;
}
}
return false;
}
function shouldInclude(elt) {
if(elt.name === "" || elt.name == null || elt.disabled) {
return false;
}
// ignore "submitter" types (see jQuery src/serialize.js)
if (elt.type === "button" || elt.type === "submit" || elt.tagName === "image" || elt.tagName === "reset" || elt.tagName === "file" ) {
return false;
}
if (elt.type === "checkbox" || elt.type === "radio" ) {
return elt.checked;
}
return true;
}
function processInputValue(processed, values, errors, elt, validate) {
if (elt == null || haveSeenNode(processed, elt)) {
return;
} else {
processed.push(elt);
}
if (shouldInclude(elt)) {
var name = getRawAttribute(elt,"name");
var value = elt.value;
if (elt.multiple) {
value = toArray(elt.querySelectorAll("option:checked")).map(function (e) { return e.value });
}
// include file inputs
if (elt.files) {
value = toArray(elt.files);
}
// This is a little ugly because both the current value of the named value in the form
// and the new value could be arrays, so we have to handle all four cases :/
if (name != null && value != null) {
var current = values[name];
if(current) {
if (Array.isArray(current)) {
if (Array.isArray(value)) {
values[name] = current.concat(value);
} else {
current.push(value);
}
} else {
if (Array.isArray(value)) {
values[name] = [current].concat(value);
} else {
values[name] = [current, value];
}
}
} else {
values[name] = value;
}
}
if (validate) {
validateElement(elt, errors);
}
}
if (matches(elt, 'form')) {
var inputs = elt.elements;
forEach(inputs, function(input) {
processInputValue(processed, values, errors, input, validate);
});
}
}
function validateElement(element, errors) {
if (element.willValidate) {
triggerEvent(element, "htmx:validation:validate")
if (!element.checkValidity()) {
errors.push({elt: element, message:element.validationMessage, validity:element.validity});
triggerEvent(element, "htmx:validation:failed", {message:element.validationMessage, validity:element.validity})
}
}
}
/**
* @param {HTMLElement} elt
* @param {string} verb
*/
function getInputValues(elt, verb) {
var processed = [];
var values = {};
var formValues = {};
var errors = [];
// only validate when form is directly submitted and novalidate is not set
var validate = matches(elt, 'form') && elt.noValidate !== true;
// for a non-GET include the closest form
if (verb !== 'get') {
processInputValue(processed, formValues, errors, closest(elt, 'form'), validate);
}
// include the element itself
processInputValue(processed, values, errors, elt, validate);
// if a button or submit was clicked last, include its value
var internalData = getInternalData(elt);
if (internalData.lastButtonClicked) {
var name = getRawAttribute(internalData.lastButtonClicked,"name");
if (name) {
values[name] = internalData.lastButtonClicked.value;
}
}
// include any explicit includes
var includes = getClosestAttributeValue(elt, "hx-include");
if (includes) {
var nodes = querySelectorAllExt(elt, includes);
forEach(nodes, function(node) {
processInputValue(processed, values, errors, node, validate);
// if a non-form is included, include any input values within it
if (!matches(node, 'form')) {
forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {
processInputValue(processed, values, errors, descendant, validate);
})
}
});
}
// form values take precedence, overriding the regular values
values = mergeObjects(values, formValues);
return {errors:errors, values:values};
}
function appendParam(returnStr, name, realValue) {
if (returnStr !== "") {
returnStr += "&";
}
returnStr += encodeURIComponent(name) + "=" + encodeURIComponent(realValue);
return returnStr;
}
function urlEncode(values) {
var returnStr = "";
for (var name in values) {
if (values.hasOwnProperty(name)) {
var value = values[name];
if (Array.isArray(value)) {
forEach(value, function(v) {
returnStr = appendParam(returnStr, name, v);
});
} else {
returnStr = appendParam(returnStr, name, value);
}
}
}
return returnStr;
}
function makeFormData(values) {
var formData = new FormData();
for (var name in values) {
if (values.hasOwnProperty(name)) {
var value = values[name];
if (Array.isArray(value)) {
forEach(value, function(v) {
formData.append(name, v);
});
} else {
formData.append(name, value);
}
}
}
return formData;
}
//====================================================================
// Ajax
//====================================================================
/**
* @param {HTMLElement} elt
* @param {HTMLElement} target
* @param {string} prompt
* @returns {Object} // TODO: Define/Improve HtmxHeaderSpecification
*/
function getHeaders(elt, target, prompt) {
var headers = {
"HX-Request" : "true",
"HX-Trigger" : getRawAttribute(elt, "id"),
"HX-Trigger-Name" : getRawAttribute(elt, "name"),
"HX-Target" : getAttributeValue(target, "id"),
"HX-Current-URL" : getDocument().location.href,
}
getValuesForElement(elt, "hx-headers", false, headers)
if (prompt !== undefined) {
headers["HX-Prompt"] = prompt;
}
if (getInternalData(elt).boosted) {
headers["HX-Boosted"] = "true";
}
return headers;
}
/**
* filterValues takes an object containing form input values
* and returns a new object that only contains keys that are
* specified by the closest "hx-params" attribute
* @param {Object} inputValues
* @param {HTMLElement} elt
* @returns {Object}
*/
function filterValues(inputValues, elt) {
var paramsValue = getClosestAttributeValue(elt, "hx-params");
if (paramsValue) {
if (paramsValue === "none") {
return {};
} else if (paramsValue === "*") {
return inputValues;
} else if(paramsValue.indexOf("not ") === 0) {
forEach(paramsValue.substr(4).split(","), function (name) {
name = name.trim();
delete inputValues[name];
});
return inputValues;
} else {
var newValues = {}
forEach(paramsValue.split(","), function (name) {
name = name.trim();
newValues[name] = inputValues[name];
});
return newValues;
}
} else {
return inputValues;
}
}
function isAnchorLink(elt) {
return getRawAttribute(elt, 'href') && getRawAttribute(elt, 'href').indexOf("#") >=0
}
/**
*
* @param {HTMLElement} elt
* @returns {import("./htmx").HtmxSwapSpecification}
*/
function getSwapSpecification(elt) {
var swapInfo = getClosestAttributeValue(elt, "hx-swap");
var swapSpec = {
"swapStyle" : getInternalData(elt).boosted ? 'innerHTML' : htmx.config.defaultSwapStyle,
"swapDelay" : htmx.config.defaultSwapDelay,
"settleDelay" : htmx.config.defaultSettleDelay
}
if (getInternalData(elt).boosted && !isAnchorLink(elt)) {
swapSpec["show"] = "top"
}
if (swapInfo) {
var split = splitOnWhitespace(swapInfo);
if (split.length > 0) {
swapSpec["swapStyle"] = split[0];
for (var i = 1; i < split.length; i++) {
var modifier = split[i];
if (modifier.indexOf("swap:") === 0) {
swapSpec["swapDelay"] = parseInterval(modifier.substr(5));
}
if (modifier.indexOf("settle:") === 0) {
swapSpec["settleDelay"] = parseInterval(modifier.substr(7));
}
if (modifier.indexOf("scroll:") === 0) {
var scrollSpec = modifier.substr(7);
var splitSpec = scrollSpec.split(":");
var scrollVal = splitSpec.pop();
var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
swapSpec["scroll"] = scrollVal;
swapSpec["scrollTarget"] = selectorVal;
}
if (modifier.indexOf("show:") === 0) {
var showSpec = modifier.substr(5);
var splitSpec = showSpec.split(":");
var showVal = splitSpec.pop();
var selectorVal = splitSpec.length > 0 ? splitSpec.join(":") : null;
swapSpec["show"] = showVal;
swapSpec["showTarget"] = selectorVal;
}
}
}
}
return swapSpec;
}
function encodeParamsForBody(xhr, elt, filteredParameters) {
var encodedParameters = null;
withExtensions(elt, function (extension) {
if (encodedParameters == null) {
encodedParameters = extension.encodeParameters(xhr, filteredParameters, elt);
}
});
if (encodedParameters != null) {
return encodedParameters;
} else {
if (getClosestAttributeValue(elt, "hx-encoding") === "multipart/form-data" ||
(matches(elt, "form") && getRawAttribute(elt, 'enctype') === "multipart/form-data")) {
return makeFormData(filteredParameters);
} else {
return urlEncode(filteredParameters);
}
}
}
/**
*
* @param {Element} target
* @returns {import("./htmx").HtmxSettleInfo}
*/
function makeSettleInfo(target) {
return {tasks: [], elts: [target]};
}
function updateScrollState(content, swapSpec) {
var first = content[0];
var last = content[content.length - 1];
if (swapSpec.scroll) {
var target = null;
if (swapSpec.scrollTarget) {
target = querySelectorExt(first, swapSpec.scrollTarget);
}
if (swapSpec.scroll === "top" && (first || target)) {
target = target || first;
target.scrollTop = 0;
}
if (swapSpec.scroll === "bottom" && (last || target)) {
target = target || last;
target.scrollTop = target.scrollHeight;
}
}
if (swapSpec.show) {
var target = null;
if (swapSpec.showTarget) {
var targetStr = swapSpec.showTarget;
if (swapSpec.showTarget === "window") {
targetStr = "body";
}
target = querySelectorExt(first, targetStr);
}
if (swapSpec.show === "top" && (first || target)) {
target = target || first;
target.scrollIntoView({block:'start', behavior: htmx.config.scrollBehavior});
}
if (swapSpec.show === "bottom" && (last || target)) {
target = target || last;
target.scrollIntoView({block:'end', behavior: htmx.config.scrollBehavior});
}
}
}
/**
* @param {HTMLElement} elt
* @param {string} attr
* @param {boolean=} evalAsDefault
* @param {Object=} values
* @returns {Object}
*/
function getValuesForElement(elt, attr, evalAsDefault, values) {
if (values == null) {
values = {};
}
if (elt == null) {
return values;
}
var attributeValue = getAttributeValue(elt, attr);
if (attributeValue) {
var str = attributeValue.trim();
var evaluateValue = evalAsDefault;
if (str.indexOf("javascript:") === 0) {
str = str.substr(11);
evaluateValue = true;
} else if (str.indexOf("js:") === 0) {
str = str.substr(3);
evaluateValue = true;
}
if (str.indexOf('{') !== 0) {
str = "{" + str + "}";
}
var varsValues;
if (evaluateValue) {
varsValues = maybeEval(elt,function () {return Function("return (" + str + ")")();}, {});
} else {
varsValues = parseJSON(str);
}
for (var key in varsValues) {
if (varsValues.hasOwnProperty(key)) {
if (values[key] == null) {
values[key] = varsValues[key];
}
}
}
}
return getValuesForElement(parentElt(elt), attr, evalAsDefault, values);
}
function maybeEval(elt, toEval, defaultVal) {
if (htmx.config.allowEval) {
return toEval();
} else {
triggerErrorEvent(elt, 'htmx:evalDisallowedError');
return defaultVal;
}
}
/**
* @param {HTMLElement} elt
* @param {*} expressionVars
* @returns
*/
function getHXVarsForElement(elt, expressionVars) {
return getValuesForElement(elt, "hx-vars", true, expressionVars);
}
/**
* @param {HTMLElement} elt
* @param {*} expressionVars
* @returns
*/
function getHXValsForElement(elt, expressionVars) {
return getValuesForElement(elt, "hx-vals", false, expressionVars);
}
/**
* @param {HTMLElement} elt
* @returns {Object}
*/
function getExpressionVars(elt) {
return mergeObjects(getHXVarsForElement(elt), getHXValsForElement(elt));
}
function safelySetHeaderValue(xhr, header, headerValue) {
if (headerValue !== null) {
try {
xhr.setRequestHeader(header, headerValue);
} catch (e) {
// On an exception, try to set the header URI encoded instead
xhr.setRequestHeader(header, encodeURIComponent(headerValue));
xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");
}
}
}
function getResponseURL(xhr) {
// NB: IE11 does not support this stuff
if (xhr.responseURL && typeof(URL) !== "undefined") {
try {
var url = new URL(xhr.responseURL);
return url.pathname + url.search;
} catch (e) {
triggerErrorEvent(getDocument().body, "htmx:badResponseUrl", {url: xhr.responseURL});
}
}
}
function hasHeader(xhr, regexp) {
return xhr.getAllResponseHeaders().match(regexp);
}
function ajaxHelper(verb, path, context) {
verb = verb.toLowerCase();
if (context) {
if (context instanceof Element || isType(context, 'String')) {
return issueAjaxRequest(verb, path, null, null, {
targetOverride: resolveTarget(context),
returnPromise: true
});
} else {
return issueAjaxRequest(verb, path, resolveTarget(context.source), context.event,
{
handler : context.handler,
headers : context.headers,
values : context.values,
targetOverride: resolveTarget(context.target),
returnPromise: true
});
}
} else {
return issueAjaxRequest(verb, path, null, null, {
returnPromise: true
});
}
}
function hierarchyForElt(elt) {
var arr = [];
while (elt) {
arr.push(elt);
elt = elt.parentElement;
}
return arr;
}
function issueAjaxRequest(verb, path, elt, event, etc) {
var resolve = null;
var reject = null;
etc = etc != null ? etc : {};
if(etc.returnPromise && typeof Promise !== "undefined"){
var promise = new Promise(function (_resolve, _reject) {
resolve = _resolve;
reject = _reject;
});
}
if(elt == null) {
elt = getDocument().body;
}
var responseHandler = etc.handler || handleAjaxResponse;
if (!bodyContains(elt)) {
return; // do not issue requests for elements removed from the DOM
}
var target = etc.targetOverride || getTarget(elt);
if (target == null) {
triggerErrorEvent(elt, 'htmx:targetError', {target: getAttributeValue(elt, "hx-target")});
return;
}
var eltData = getInternalData(elt);
if (eltData.requestInFlight) {
var queueStrategy = 'last';
if (event) {
var eventData = getInternalData(event);
if (eventData && eventData.triggerSpec && eventData.triggerSpec.queue) {
queueStrategy = eventData.triggerSpec.queue;
}
}
if (eltData.queuedRequests == null) {
eltData.queuedRequests = [];
}
if (queueStrategy === "first" && eltData.queuedRequests.length === 0) {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "all") {
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
} else if (queueStrategy === "last") {
eltData.queuedRequests = []; // dump existing queue
eltData.queuedRequests.push(function () {
issueAjaxRequest(verb, path, elt, event, etc)
});
}
return;
} else {
eltData.requestInFlight = true;
}
var endRequestLock = function(){
eltData.requestInFlight = false
if (eltData.queuedRequests != null &&
eltData.queuedRequests.length > 0) {
var queuedRequest = eltData.queuedRequests.shift();
queuedRequest();
}
}
var promptQuestion = getClosestAttributeValue(elt, "hx-prompt");
if (promptQuestion) {
var promptResponse = prompt(promptQuestion);
// prompt returns null if cancelled and empty string if accepted with no entry
if (promptResponse === null ||
!triggerEvent(elt, 'htmx:prompt', {prompt: promptResponse, target:target})) {
maybeCall(resolve);
endRequestLock();
return promise;
}
}
var confirmQuestion = getClosestAttributeValue(elt, "hx-confirm");
if (confirmQuestion) {
if(!confirm(confirmQuestion)) {
maybeCall(resolve);
endRequestLock()
return promise;
}
}
var xhr = new XMLHttpRequest();
var headers = getHeaders(elt, target, promptResponse);
if (etc.headers) {
headers = mergeObjects(headers, etc.headers);
}
var results = getInputValues(elt, verb);
var errors = results.errors;
var rawParameters = results.values;
if (etc.values) {
rawParameters = mergeObjects(rawParameters, etc.values);
}
var expressionVars = getExpressionVars(elt);
var allParameters = mergeObjects(rawParameters, expressionVars);
var filteredParameters = filterValues(allParameters, elt);
if (verb !== 'get' && getClosestAttributeValue(elt, "hx-encoding") == null) {
headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
}
// behavior of anchors w/ empty href is to use the current URL
if (path == null || path === "") {
path = getDocument().location.href;
}
var requestAttrValues = getValuesForElement(elt, 'hx-request');
var requestConfig = {
parameters: filteredParameters,
unfilteredParameters: allParameters,
headers:headers,
target:target,
verb:verb,
errors:errors,
withCredentials: etc.credentials || requestAttrValues.credentials || htmx.config.withCredentials,
timeout: etc.timeout || requestAttrValues.timeout || htmx.config.timeout,
path:path,
triggeringEvent:event
};
if(!triggerEvent(elt, 'htmx:configRequest', requestConfig)){
maybeCall(resolve);
endRequestLock();
return promise;
}
// copy out in case the object was overwritten
path = requestConfig.path;
verb = requestConfig.verb;
headers = requestConfig.headers;
filteredParameters = requestConfig.parameters;
errors = requestConfig.errors;
if(errors && errors.length > 0){
triggerEvent(elt, 'htmx:validation:halted', requestConfig)
maybeCall(resolve);
endRequestLock();
return promise;
}
var splitPath = path.split("#");
var pathNoAnchor = splitPath[0];
var anchor = splitPath[1];
if (verb === 'get') {
var finalPathForGet = pathNoAnchor;
var values = Object.keys(filteredParameters).length !== 0;
if (values) {
if (finalPathForGet.indexOf("?") < 0) {
finalPathForGet += "?";
} else {
finalPathForGet += "&";
}
finalPathForGet += urlEncode(filteredParameters);
if (anchor) {
finalPathForGet += "#" + anchor;
}
}
xhr.open('GET', finalPathForGet, true);
} else {
xhr.open(verb.toUpperCase(), path, true);
}
xhr.overrideMimeType("text/html");
xhr.withCredentials = requestConfig.withCredentials;
xhr.timeout = requestConfig.timeout;
// request headers
if (requestAttrValues.noHeaders) {
// ignore all headers
} else {
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
var headerValue = headers[header];
safelySetHeaderValue(xhr, header, headerValue);
}
}
}
var responseInfo = {xhr: xhr, target: target, requestConfig: requestConfig, pathInfo:{
path:path, finalPath:finalPathForGet, anchor:anchor
}
};
xhr.onload = function () {
try {
var hierarchy = hierarchyForElt(elt);
responseHandler(elt, responseInfo);
removeRequestIndicatorClasses(indicators);
triggerEvent(elt, 'htmx:afterRequest', responseInfo);
triggerEvent(elt, 'htmx:afterOnLoad', responseInfo);
// if the body no longer contains the element, trigger the even on the closest parent
// remaining in the DOM
if (!bodyContains(elt)) {
var secondaryTriggerElt = null;
while (hierarchy.length > 0 && secondaryTriggerElt == null) {
var parentEltInHierarchy = hierarchy.shift();
if (bodyContains(parentEltInHierarchy)) {
secondaryTriggerElt = parentEltInHierarchy;
}
}
if (secondaryTriggerElt) {
triggerEvent(secondaryTriggerElt, 'htmx:afterRequest', responseInfo);
triggerEvent(secondaryTriggerElt, 'htmx:afterOnLoad', responseInfo);
}
}
maybeCall(resolve);
endRequestLock();
} catch (e) {
triggerErrorEvent(elt, 'htmx:onLoadError', mergeObjects({error:e}, responseInfo));
throw e;
}
}
xhr.onerror = function () {
removeRequestIndicatorClasses(indicators);
triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
triggerErrorEvent(elt, 'htmx:sendError', responseInfo);
maybeCall(reject);
endRequestLock();
}
xhr.onabort = function() {
removeRequestIndicatorClasses(indicators);
triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
triggerErrorEvent(elt, 'htmx:sendAbort', responseInfo);
maybeCall(reject);
endRequestLock();
}
xhr.ontimeout = function() {
removeRequestIndicatorClasses(indicators);
triggerErrorEvent(elt, 'htmx:afterRequest', responseInfo);
triggerErrorEvent(elt, 'htmx:timeout', responseInfo);
maybeCall(reject);
endRequestLock();
}
if(!triggerEvent(elt, 'htmx:beforeRequest', responseInfo)){
maybeCall(resolve);
endRequestLock()
return promise
}
var indicators = addRequestIndicatorClasses(elt);
forEach(['loadstart', 'loadend', 'progress', 'abort'], function(eventName) {
forEach([xhr, xhr.upload], function (target) {
target.addEventListener(eventName, function(event){
triggerEvent(elt, "htmx:xhr:" + eventName, {
lengthComputable:event.lengthComputable,
loaded:event.loaded,
total:event.total
});
})
});
});
triggerEvent(elt, 'htmx:beforeSend', responseInfo);
xhr.send(verb === 'get' ? null : encodeParamsForBody(xhr, elt, filteredParameters));
return promise;
}
function handleAjaxResponse(elt, responseInfo) {
var xhr = responseInfo.xhr;
var target = responseInfo.target;
if (!triggerEvent(elt, 'htmx:beforeOnLoad', responseInfo)) return;
if (hasHeader(xhr, /HX-Trigger:/i)) {
handleTrigger(xhr, "HX-Trigger", elt);
}
if (hasHeader(xhr,/HX-Push:/i)) {
var pushedUrl = xhr.getResponseHeader("HX-Push");
}
if (hasHeader(xhr, /HX-Redirect:/i)) {
window.location.href = xhr.getResponseHeader("HX-Redirect");
return;
}
if (hasHeader(xhr,/HX-Refresh:/i)) {
if ("true" === xhr.getResponseHeader("HX-Refresh")) {
location.reload();
return;
}
}
if (hasHeader(xhr,/HX-Retarget:/i)) {
responseInfo.target = getDocument().querySelector(xhr.getResponseHeader("HX-Retarget"));
}
var shouldSaveHistory = shouldPush(elt) || pushedUrl;
// by default htmx only swaps on 200 return codes and does not swap
// on 204 'No Content'
// this can be ovverriden by responding to the htmx:beforeSwap event and
// overriding the detail.shouldSwap property
var shouldSwap = xhr.status >= 200 && xhr.status < 400 && xhr.status !== 204;
var serverResponse = xhr.response;
var isError = xhr.status >= 400;
var beforeSwapDetails = mergeObjects({shouldSwap: shouldSwap, serverResponse:serverResponse, isError:isError}, responseInfo);
if (!triggerEvent(target, 'htmx:beforeSwap', beforeSwapDetails)) return;
target = beforeSwapDetails.target; // allow re-targeting
serverResponse = beforeSwapDetails.serverResponse; // allow updating content
isError = beforeSwapDetails.isError; // allow updating error
responseInfo.failed = isError; // Make failed property available to response events
responseInfo.successful = !isError; // Make successful property available to response events
if (beforeSwapDetails.shouldSwap) {
if (xhr.status === 286) {
cancelPolling(elt);
}
withExtensions(elt, function (extension) {
serverResponse = extension.transformResponse(serverResponse, xhr, elt);
});
// Save current page
if (shouldSaveHistory) {
saveHistory();
}
var swapSpec = getSwapSpecification(elt);
target.classList.add(htmx.config.swappingClass);
var doSwap = function () {
try {
var activeElt = document.activeElement;
var selectionInfo = {};
try {
selectionInfo = {
elt: activeElt,
// @ts-ignore
start: activeElt ? activeElt.selectionStart : null,
// @ts-ignore
end: activeElt ? activeElt.selectionEnd : null
};
} catch (e) {
// safari issue - see https://github.com/microsoft/playwright/issues/5894
}
var settleInfo = makeSettleInfo(target);
selectAndSwap(swapSpec.swapStyle, target, elt, serverResponse, settleInfo);
if (selectionInfo.elt &&
!bodyContains(selectionInfo.elt) &&
selectionInfo.elt.id) {
var newActiveElt = document.getElementById(selectionInfo.elt.id);
if (newActiveElt) {
// @ts-ignore
if (selectionInfo.start && newActiveElt.setSelectionRange) {
// @ts-ignore
newActiveElt.setSelectionRange(selectionInfo.start, selectionInfo.end);
}
newActiveElt.focus();
}
}
target.classList.remove(htmx.config.swappingClass);
forEach(settleInfo.elts, function (elt) {
if (elt.classList) {
elt.classList.add(htmx.config.settlingClass);
}
triggerEvent(elt, 'htmx:afterSwap', responseInfo);
});
if (responseInfo.pathInfo.anchor) {
location.hash = responseInfo.pathInfo.anchor;
}
if (hasHeader(xhr, /HX-Trigger-After-Swap:/i)) {
var finalElt = elt;
if (!bodyContains(elt)) {
finalElt = getDocument().body;
}
handleTrigger(xhr, "HX-Trigger-After-Swap", finalElt);
}
var doSettle = function () {
forEach(settleInfo.tasks, function (task) {
task.call();
});
forEach(settleInfo.elts, function (elt) {
if (elt.classList) {
elt.classList.remove(htmx.config.settlingClass);
}
triggerEvent(elt, 'htmx:afterSettle', responseInfo);
});
// push URL and save new page
if (shouldSaveHistory) {
var pathToPush = pushedUrl || getPushUrl(elt) || getResponseURL(xhr) || responseInfo.pathInfo.finalPath || responseInfo.pathInfo.path;
pushUrlIntoHistory(pathToPush);
triggerEvent(getDocument().body, 'htmx:pushedIntoHistory', {path: pathToPush});
}
updateScrollState(settleInfo.elts, swapSpec);
if (hasHeader(xhr, /HX-Trigger-After-Settle:/i)) {
var finalElt = elt;
if (!bodyContains(elt)) {
finalElt = getDocument().body;
}
handleTrigger(xhr, "HX-Trigger-After-Settle", finalElt);
}
}
if (swapSpec.settleDelay > 0) {
setTimeout(doSettle, swapSpec.settleDelay)
} else {
doSettle();
}
} catch (e) {
triggerErrorEvent(elt, 'htmx:swapError', responseInfo);
throw e;
}
};
if (swapSpec.swapDelay > 0) {
setTimeout(doSwap, swapSpec.swapDelay)
} else {
doSwap();
}
}
if (isError) {
triggerErrorEvent(elt, 'htmx:responseError', mergeObjects({error: "Response Status Error Code " + xhr.status + " from " + responseInfo.pathInfo.path}, responseInfo));
}
}
//====================================================================
// Extensions API
//====================================================================
/** @type {Object<string, import("./htmx").HtmxExtension>} */
var extensions = {};
/**
* extensionBase defines the default functions for all extensions.
* @returns {import("./htmx").HtmxExtension}
*/
function extensionBase() {
return {
init: function(api) {return null;},
onEvent : function(name, evt) {return true;},
transformResponse : function(text, xhr, elt) {return text;},
isInlineSwap : function(swapStyle) {return false;},
handleSwap : function(swapStyle, target, fragment, settleInfo) {return false;},
encodeParameters : function(xhr, parameters, elt) {return null;}
}
}
/**
* defineExtension initializes the extension and adds it to the htmx registry
*
* @param {string} name
* @param {import("./htmx").HtmxExtension} extension
*/
function defineExtension(name, extension) {
extension.init(internalAPI)
extensions[name] = mergeObjects(extensionBase(), extension);
}
/**
* removeExtension removes an extension from the htmx registry
*
* @param {string} name
*/
function removeExtension(name) {
delete extensions[name];
}
/**
* getExtensions searches up the DOM tree to return all extensions that can be applied to a given element
*
* @param {HTMLElement} elt
* @param {import("./htmx").HtmxExtension[]=} extensionsToReturn
* @param {import("./htmx").HtmxExtension[]=} extensionsToIgnore
*/
function getExtensions(elt, extensionsToReturn, extensionsToIgnore) {
if (elt == undefined) {
return extensionsToReturn;
}
if (extensionsToReturn == undefined) {
extensionsToReturn = [];
}
if (extensionsToIgnore == undefined) {
extensionsToIgnore = [];
}
var extensionsForElement = getAttributeValue(elt, "hx-ext");
if (extensionsForElement) {
forEach(extensionsForElement.split(","), function(extensionName){
extensionName = extensionName.replace(/ /g, '');
if (extensionName.slice(0, 7) == "ignore:") {
extensionsToIgnore.push(extensionName.slice(7));
return;
}
if (extensionsToIgnore.indexOf(extensionName) < 0) {
var extension = extensions[extensionName];
if (extension && extensionsToReturn.indexOf(extension) < 0) {
extensionsToReturn.push(extension);
}
}
});
}
return getExtensions(parentElt(elt), extensionsToReturn, extensionsToIgnore);
}
//====================================================================
// Initialization
//====================================================================
function ready(fn) {
if (getDocument().readyState !== 'loading') {
fn();
} else {
getDocument().addEventListener('DOMContentLoaded', fn);
}
}
function insertIndicatorStyles() {
if (htmx.config.includeIndicatorStyles !== false) {
getDocument().head.insertAdjacentHTML("beforeend",
"<style>\
." + htmx.config.indicatorClass + "{opacity:0;transition: opacity 200ms ease-in;}\
." + htmx.config.requestClass + " ." + htmx.config.indicatorClass + "{opacity:1}\
." + htmx.config.requestClass + "." + htmx.config.indicatorClass + "{opacity:1}\
</style>");
}
}
function getMetaConfig() {
var element = getDocument().querySelector('meta[name="htmx-config"]');
if (element) {
// @ts-ignore
return parseJSON(element.content);
} else {
return null;
}
}
function mergeMetaConfig() {
var metaConfig = getMetaConfig();
if (metaConfig) {
htmx.config = mergeObjects(htmx.config , metaConfig)
}
}
// initialize the document
ready(function () {
mergeMetaConfig();
insertIndicatorStyles();
var body = getDocument().body;
processNode(body);
window.onpopstate = function (event) {
if (event.state && event.state.htmx) {
restoreHistory();
}
};
setTimeout(function () {
triggerEvent(body, 'htmx:load', {}); // give ready handlers a chance to load up before firing this event
}, 0);
})
return htmx;
}
)()
}));
package graphStore
import (
"fmt"
"webster/MemoryLane/global"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/graphstorage"
"github.com/rs/zerolog/log"
)
var _initialized = false
var _gs graphstorage.Storage
var _gpart string
var _gm *graph.Manager
func GetGraphStore() graphstorage.Storage {
if !_initialized {
panic("GraphStore not initialized")
}
return _gs
}
func GetGraphManager() *graph.Manager {
if !_initialized {
panic("GraphStore not initialized")
}
return _gm
}
func GetGpart() string {
if !_initialized {
panic("GraphStore not initialized")
}
return _gpart
}
func InitGraphStore(name string, gpart string) error {
log.Logger.Debug().Caller().Msgf("Initializing Connection to EliasDB %s.", name)
if !_initialized {
// Create a graph storage
gs, err := graphstorage.NewDiskGraphStorage(name, false)
_gs = gs
_gpart = gpart
if err != nil {
log.Logger.Fatal().Err(err).Msg("Could not create graph storage")
return err
}
gm := graph.NewGraphManager(_gs)
_gm = gm
_initialized = true
global.RegisterShutdownAction(func() {
log.Info().Msg("Closing Graph Storage.")
fmt.Println("Closing Graph Storage .")
_gs.Close()
})
}
return nil
}
package graphStore
import (
"os"
"time"
"devt.de/krotik/eliasdb/graph"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func Backup(gpart string, gm *graph.Manager) error {
tmpPath := viper.GetString("TempLocation")
backupPath := tmpPath + "/backup"
backupFile := backupPath + "/backup - " + time.Now().Format(time.RFC850) + ".json"
err := os.MkdirAll(backupPath, 0755)
if err != nil {
log.Error().Err(err).Msg("Error creating temp directory")
return err
}
file, err := os.Create(backupFile)
if err != nil {
log.Error().Err(err).Msg("Could not create backup file")
return err
}
defer file.Close()
err = graph.ExportPartition(file, gpart, gm)
if err != nil {
log.Error().Err(err).Msg("Could not create backup file")
return err
}
return nil
}
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
devt.de/krotik/common v1.4.0/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
devt.de/krotik/common v1.4.1/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
devt.de/krotik/common v1.4.4 h1:6FGLqqIU1HN5vRMydUud2DMcEfGBQyDTewegcblRMO4=
devt.de/krotik/common v1.4.4/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
devt.de/krotik/ecal v1.6.1/go.mod h1:0qIx3h+EjUnStgdEUnwAeO44UluTSLcpBWXA5zEw0hQ=
devt.de/krotik/eliasdb v1.2.0 h1:csj0nA/xRBC9mwkNKo6upott1WhCTtAnnJGyvd9A//s=
devt.de/krotik/eliasdb v1.2.0/go.mod h1:2akB7OY5k+y0TWf4Y0RTJe1r+kZtduXq42GxHQvTZzk=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Xuanwo/gg v0.2.0/go.mod h1:0fLiiSxR87u2UA0ZNZiKZXuz3jnJdbDHWtU2xpdcH3s=
github.com/Xuanwo/gg v0.3.0 h1:jHasK7tJ4o/IjpcxPbabQ4zVO+hln85DvNYhq5GamcA=
github.com/Xuanwo/gg v0.3.0/go.mod h1:0fLiiSxR87u2UA0ZNZiKZXuz3jnJdbDHWtU2xpdcH3s=
github.com/Xuanwo/go-bufferpool v0.2.0 h1:DXzqJD9lJufXbT/03GrcEvYOs4gXYUj9/g5yi6Q9rUw=
github.com/Xuanwo/go-bufferpool v0.2.0/go.mod h1:Mle++9GGouhOwGj52i9PJLNAPmW2nb8PWBP7JJzNCzk=
github.com/Xuanwo/templateutils v0.1.0/go.mod h1:OdE0DJ+CJxDBq6psX5DPV+gOZi8bhuHuVUpPCG++Wb8=
github.com/Xuanwo/templateutils v0.2.0 h1:jnhiP1DMyK1Rv9qgaGCrEm/r6TseAsf7eC092gVld0Q=
github.com/Xuanwo/templateutils v0.2.0/go.mod h1:OdE0DJ+CJxDBq6psX5DPV+gOZi8bhuHuVUpPCG++Wb8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.10.0/go.mod h1:U/EyyVvKtzmFeQQcca7eBotKdlpcP2zzU6bXBYcf7CE=
github.com/aws/aws-sdk-go-v2 v1.11.2 h1:SDiCYqxdIYi6HgQfAWRhgdZrdnOuGyLDJVRSWLeHWvs=
github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 h1:yVUAwvJC/0WNPbyl0nA3j1L6CW1CN8wBubCRqtG7JLI=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0/go.mod h1:Xn6sxgRuIDflLRJFj5Ev7UxABIkNbccFPV/p8itDReM=
github.com/aws/aws-sdk-go-v2/config v1.9.0/go.mod h1:qhK5NNSgo9/nOSMu3HyE60WHXZTWTHTgd5qtIF44vOQ=
github.com/aws/aws-sdk-go-v2/config v1.11.0 h1:Czlld5zBB61A3/aoegA9/buZulwL9mHHfizh/Oq+Kqs=
github.com/aws/aws-sdk-go-v2/config v1.11.0/go.mod h1:VrQDJGFBM5yZe+IOeenNZ/DWoErdny+k2MHEIpwDsEY=
github.com/aws/aws-sdk-go-v2/credentials v1.5.0/go.mod h1:kvqTkpzQmzri9PbsiTY+LvwFzM0gY19emlAWwBOJMb0=
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 h1:2hvbUoHufns0lDIsaK8FVCMukT1WngtZPavN+W2FkSw=
github.com/aws/aws-sdk-go-v2/credentials v1.6.4/go.mod h1:tTrhvBPHyPde4pdIPSba4Nv7RYr4wP9jxXEDa1bKn/8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.7.0/go.mod h1:KqEkRkxm/+1Pd/rENRNbQpfblDBYeg5HDSqjB6ks8hA=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 h1:KiN5TPOLrEjbGCvdTQR4t0U4T87vVwALZ5Bg3jpMqPY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2/go.mod h1:dF2F6tXEOgmW5X1ZFO/EPtWrcm7XkW07KNcJUGNtt4s=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 h1:XJLnluKuUxQG255zPNe+04izXl7GSyUVafIsgfv9aw4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 h1:EauRoYZVNPlidZSZJDscjJBQ22JhVF2+tdteatax2Ak=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU=
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.5/go.mod h1:6ZBTuDmvpCOD4Sf1i2/I3PgftlEcDGgvi8ocq64oQEg=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 h1:IQup8Q6lorXeiA/rK72PeToWoWK8h7VAPgHNWdSrtgE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2/go.mod h1:VITe/MdW6EMXPb0o0txu/fsonXbMHUU2OC2Qp7ivU4o=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.4.0/go.mod h1:vEkJTjJ8vnv0uWy2tAp7DSydWFpudMGWPQ2SFucoN1k=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0/go.mod h1:80NaCIH9YU3rzTTs/J/ECATjXuRqzo/wB6ukO6MZ0XY=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.4.0/go.mod h1:X5/JuOxPLU/ogICgDTtnpfaQzdQJO0yKDcpoxWLLJ8Y=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 h1:CKdUNKmuilw/KNmO2Q53Av8u+ZyXMC2M9aX8Z+c/gzg=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2/go.mod h1:FgR1tCsn8C6+Hf+N5qkfrE4IXvUL1RgW87sunJ+5J4I=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.8.0/go.mod h1:669UCOYqQ7jA8sqwEsbIXoYrfp8KT9BeUrST0/mhCFw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 h1:GnPGH1FGc4fkn0Jbm/8r2+nPOwSJjYPyHSqFSvY1ii8=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2/go.mod h1:eDUYjOYt4Uio7xfHi5jOsO393ZG8TSfZB92a3ZNadWM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.17.0/go.mod h1:6mvopTtbyJcY0NfSOVtgkBlDDatYwiK1DAFr4VL0QCo=
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0 h1:vUM2P60BI755i35Gyik4s/lXKcnpEbnvw2Vud+soqpI=
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0/go.mod h1:lQ5AeEW2XWzu8hwQ3dCqZFWORQ3RntO0Kq135Xd9VCo=
github.com/aws/aws-sdk-go-v2/service/sso v1.5.0/go.mod h1:GsqaJOJeOfeYD88/2vHWKXegvDRofDqWwC5i48A2kgs=
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 h1:2IDmvSb86KT44lSg1uU4ONpzgWLOuApRl6Tg54mZ6Dk=
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2/go.mod h1:KnIpszaIdwI33tmc/W/GGXyn22c1USYxA/2KyvoeDY0=
github.com/aws/aws-sdk-go-v2/service/sts v1.8.0/go.mod h1:dOlm91B439le5y1vtPCk5yJtbx3RdT3hRGYRY8TYKvQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZgUevSSJ4wxmyWXk=
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA=
github.com/aws/smithy-go v1.8.1/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/buckket/go-blurhash v1.1.0 h1:X5M6r0LIvwdvKiUtiNcRL2YlmOfMzYobI3VCKCZc9Do=
github.com/buckket/go-blurhash v1.1.0/go.mod h1:aT2iqo5W9vu9GpyoLErKfTHwgODsZp3bQfXjXJUxNb8=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/christophersw/gomponents-htmx v0.20.0 h1:cV4q0GdtvpTcFdQHFMkFBMI8WxLxtkqzLUpLY+9gq7I=
github.com/christophersw/gomponents-htmx v0.20.0/go.mod h1:Uu4ui8mI3IfunXFhdVubrdcD8MDO9U+7mKbbQh0y6AM=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/dave/dst v0.26.2 h1:lnxLAKI3tx7MgLNVDirFCsDTlTG9nKTk7GcptKcWSwY=
github.com/dave/dst v0.26.2/go.mod h1:UMDJuIRPfyUCC78eFuB+SV/WI8oDeyFDvM/JR6NI3IU=
github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ=
github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8=
github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA=
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/davidbyttow/govips/v2 v2.7.0 h1:KWlSrKhgzkxgZeFAUl+3RLCJMnBzyL+tcawU/fxRPEo=
github.com/davidbyttow/govips/v2 v2.7.0/go.mod h1:goq38QD8XEMz2aWEeucEZqRxAWsemIN40vbUqfPfTAw=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b h1:NgNuLvW/gAFKU30ULWW0gtkCt56JfB7FrZ2zyo0wT8I=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf h1:/w4QxepU4AHh3AuO6/g8y/YIIHH5+aKP3Bj8sg5cqhU=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.1 h1:IvVlgbzSsaUNudsw5dcXSzF3EWyXTi5XrAdngnuhRyg=
github.com/go-errors/errors v1.4.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181127221834-b4f47329b966/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/csrf v1.7.1 h1:Ir3o2c1/Uzj6FBxMlAUB6SivgVMy1ONXwYgXn+/aHPE=
github.com/gorilla/csrf v1.7.1/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kevinburke/go-bindata v3.22.0+incompatible h1:/JmqEhIWQ7GRScV0WjX/0tqBrC5D21ALg0H0U/KZ/ts=
github.com/kevinburke/go-bindata v3.22.0+incompatible/go.mod h1:/pEEZ72flUW2p0yi30bslSp9YqD9pysLxunQDdb2CPM=
github.com/kinsey40/pbar v0.0.0-20190815161936-21f8229eaa8a h1:Sn0FUsz+HHYf2ZwvGTmak6WtuG6FFoUpCYZ/bMC4W4I=
github.com/kinsey40/pbar v0.0.0-20190815161936-21f8229eaa8a/go.mod h1:QwUrOchRIYL3j2lQBls246ox3I5JdnVlbsdVqrvg6kI=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
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/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/maragudk/gomponents v0.18.0 h1:EcdeRUWsWW6hK9ftnGAoyXR00SQWecCBxuXghQvdEcc=
github.com/maragudk/gomponents v0.18.0/go.mod h1:0OdlqOoqxcwvhBFrp8wlKHnEXhNB7IVhb8GuARmd+tI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/qingstor/go-mime v0.1.0 h1:FhTJtM7TRm9pfgCXpjGUxqwbumGojrgE9ecRz5PXvfc=
github.com/qingstor/go-mime v0.1.0/go.mod h1:EDwWgaMufg74m7futsF0ZGkdA52ajjAycY+XDeV8M88=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE=
github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.6/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.10.0 h1:mXH0UwHS4D2HwWZa75im4xIQynLfblmWV7qcWpfv0yk=
github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w=
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.beyondstorage.io/credential v1.0.0 h1:xJ7hBXmeUE0+rbW+RYZSz4KgHpXvc9g7oQ56f8dXdBk=
go.beyondstorage.io/credential v1.0.0/go.mod h1:7KAYievVw4a8u/eLZmnQt65Z91n84sMQj3LFbt8Xous=
go.beyondstorage.io/endpoint v1.2.0 h1:/7mgKquTykeqJ9op82hso2+WQfECeywGd/Lda1N3tF4=
go.beyondstorage.io/endpoint v1.2.0/go.mod h1:oZ7Z7HZ7mAo337JBLjuCF/DM66HVEUu6+hw68c3UcLs=
go.beyondstorage.io/services/fs/v4 v4.0.0 h1:ukDNhoUI1E5x6DDDRUFaA6JbOdrE0taDHKiOemLqnL8=
go.beyondstorage.io/services/fs/v4 v4.0.0/go.mod h1:gxqLiBwoQQBt1xHTUw2js59tXiYiW67M/RzbP3FwRUc=
go.beyondstorage.io/services/s3/v3 v3.0.1 h1:ccNdY88tU3AenaaCAZlYCHD2hZD7ozc9anrmfDse/RU=
go.beyondstorage.io/services/s3/v3 v3.0.1/go.mod h1:2Zk3+uLbaGbGfANR+pBx3tGPIaWh1s8Wve5xt1mloQM=
go.beyondstorage.io/v5 v5.0.0 h1:k9Axfgbt+oZXoDwSBVCl1XANHSL4rkNTGP2Lz9YdJe0=
go.beyondstorage.io/v5 v5.0.0/go.mod h1:3wV9gCQnqu7tD/3LMeo2yimUKIeTSHpTc6wHSb0yY20=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
golang.org/x/arch v0.0.0-20180920145803-b19384d3c130/go.mod h1:cYlCBUl1MsqxdiKgmc4uh7TxZfWSFLOGSRR090WDxt8=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b h1:QAqMVf3pSa6eeTsuklijukjXBlj7Es2QQplab+/RbQ4=
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211209124913-491a49abca63 h1:iocB37TsdFuN6IBRZ+ry36wrkoV51/tl5vOWqkcPGvY=
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211214170744-3b038e5940ed h1:d5glpD+GMms2DMbu1doSYibjbKasYNvnhq885nOnRz8=
golang.org/x/sys v0.0.0-20211214170744-3b038e5940ed/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
module webster/MemoryLane
go 1.17
require (
devt.de/krotik/eliasdb v1.2.0
github.com/christophersw/gomponents-htmx v0.20.0
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b
github.com/go-chi/chi/v5 v5.0.7
github.com/kinsey40/pbar v0.0.0-20190815161936-21f8229eaa8a
github.com/maragudk/gomponents v0.18.0
github.com/rs/zerolog v1.26.0
github.com/spf13/cobra v1.3.0
github.com/spf13/viper v1.10.0
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125
go.beyondstorage.io/services/s3/v3 v3.0.1
go.beyondstorage.io/v5 v5.0.0
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
)
require (
devt.de/krotik/common v1.4.4 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-errors/errors v1.4.1 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/qingstor/go-mime v0.1.0 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/Xuanwo/gg v0.3.0 // indirect
github.com/Xuanwo/go-bufferpool v0.2.0 // indirect
github.com/Xuanwo/templateutils v0.2.0 // indirect
github.com/aws/aws-sdk-go-v2 v1.11.2 // indirect
github.com/aws/aws-sdk-go-v2/config v1.11.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
github.com/aws/smithy-go v1.9.0 // indirect
github.com/buckket/go-blurhash v1.1.0
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/dave/dst v0.26.2 // indirect
github.com/davidbyttow/govips/v2 v2.7.0
github.com/gorilla/csrf v1.7.1
github.com/gorilla/securecookie v1.1.1
github.com/kevinburke/go-bindata v3.22.0+incompatible // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/urfave/cli/v2 v2.3.0 // indirect
go.beyondstorage.io/credential v1.0.0 // indirect
go.beyondstorage.io/endpoint v1.2.0 // indirect
go.beyondstorage.io/services/fs/v4 v4.0.0
golang.org/x/mod v0.5.1 // indirect
golang.org/x/sys v0.0.0-20211214170744-3b038e5940ed // indirect
golang.org/x/tools v0.1.8 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
)
package global
import (
"fmt"
"time"
)
var _shutdownActions []func()
func Shutdown() {
fmt.Println("Shut Down Initiated, giving things 30s wrap up...")
time.Sleep(time.Second * 30)
for _, f := range _shutdownActions {
f()
}
}
func RegisterShutdownAction(f func()) {
_shutdownActions = append(_shutdownActions, f)
}
package global
import (
"fmt"
"os"
"time"
"github.com/davidbyttow/govips/v2/vips"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func SetupLogger() func() error {
//Set the Global Log Level
logLevel := viper.GetString("LogLevel")
switch logLevel {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
case "fatal":
zerolog.SetGlobalLevel(zerolog.FatalLevel)
default:
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
//Set the log output
log.Debug().Msg("Setting Log output to:" + viper.GetString("LogOutput"))
logOut := viper.GetString("LogOutput")
if !viper.IsSet("LogOutput") {
logOut = "log - " + time.Now().Format(time.RFC850) + ".json"
}
if logOut == "stdout" {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
return func() error { return nil }
} else {
file, err := os.Create(logOut)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
log.Logger = log.Output(file)
return file.Close
}
}
func VipsLogger(messageDomain string, verbosity vips.LogLevel, message string) {
message = "VIPS:" + message
switch verbosity {
case vips.LogLevelError:
log.Error().Msg(message)
case vips.LogLevelCritical:
log.Warn().Msg(message)
case vips.LogLevelWarning:
log.Warn().Msg(message)
case vips.LogLevelMessage:
log.Log().Msg(message)
case vips.LogLevelInfo:
log.Info().Msg(message)
case vips.LogLevelDebug:
log.Debug().Msg(message)
}
}
package global
import (
"context"
)
var _ctx context.Context
func GetGlobalContext() context.Context {
if _ctx == nil {
_ctx := context.Background()
return _ctx
}
return _ctx
}
func SetGlobalContext(ctx context.Context) {
_ctx = ctx
}
package data
import "sort"
type Result struct {
Key string
Score int
}
type SearchResults []Result
func (r SearchResults) Less(i, j int) bool {
return r[i].Score > r[j].Score
}
func (r SearchResults) Len() int {
return len(r)
}
func (r SearchResults) Swap(i, j int) {
r[i], r[j] = r[j], r[i]
}
func ResultsFromMap(m map[string][]uint64) SearchResults {
var results SearchResults
for k, v := range m {
results = append(results, Result{k, len(v)})
}
return results
}
func (r SearchResults) Sort() {
sort.Sort(r)
}
func GetSortedResultKeys(m map[string][]uint64) []string {
results := ResultsFromMap(m)
results.Sort()
var keys []string
for _, r := range results {
keys = append(keys, r.Key)
}
return keys
}
package data
import (
"errors"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/spf13/viper"
"github.com/teris-io/shortid"
"golang.org/x/crypto/bcrypt"
)
type User struct {
GraphNode GraphNode
UserName string `json:"name"`
Email string `json:"email"`
Favorites []GraphNode `json:"favorites"`
}
var _usersInitialized = false
func ScaffoldUsers() error {
_usersInitialized = true
scaffoldUser := viper.GetStringMapString("DefaultUser")
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
existing, err := GetAllUsers(gm, gpart)
if err != nil {
return err
}
if len(existing) == 0 {
user := User{
UserName: scaffoldUser["username"],
Email: scaffoldUser["email"],
}
err = user.Upsert(gm, gpart)
if err != nil {
return err
}
err = user.SetPassword(scaffoldUser["password"], gm, gpart)
if err != nil {
return err
}
}
return nil
}
func (u *User) GetUpsertNode(gm *graph.Manager, gpart string) (node data.Node, err error) {
existing, err := GetUserByKey(u.GraphNode.Key, false, gm, gpart)
if err != nil {
return nil, err
}
// If there is an existing user, then we need to update it, otherwise we need to create a new one
if existing.GraphNode.Key != "" {
u.GraphNode.Key = existing.GraphNode.Key
} else {
// Check to see if we can find by username
user, err := GetUserByUsername(u.UserName, gm, gpart)
if err != nil {
return nil, err
}
if user.GraphNode.Key != "" {
return nil, errors.New("username already exists")
}
// Check to see if we can find by email
user, err = GetUserByEmail(u.Email, gm, gpart)
if err != nil {
return nil, err
}
if user.GraphNode.Key != "" {
return nil, errors.New("email already exists")
}
u.GraphNode.Key = shortid.MustGenerate()
}
u.GraphNode.Kind = "user"
userNode := data.NewGraphNode()
userNode.SetAttr("key", u.GraphNode.Key)
userNode.SetAttr("kind", u.GraphNode.Kind)
userNode.SetAttr("name", u.UserName)
userNode.SetAttr("email", u.Email)
// What about password hash? This can ONLY be set using setPassword()
return userNode, nil
}
func (u *User) Upsert(gm *graph.Manager, gpart string) error {
node, err := u.GetUpsertNode(gm, gpart)
if err != nil {
return err
}
trans := graph.NewGraphTrans(gm)
if err := trans.StoreNode(gpart, node); err != nil {
return nil
}
return trans.Commit()
}
func (u *User) SetPassword(password string, gm *graph.Manager, gpart string) error {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
passwordBytes := []byte(password)
// Hashing the password with the default cost of 10
hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
if err != nil {
return err
}
node := data.NewGraphNode()
node.SetAttr("key", u.GraphNode.Key)
node.SetAttr("kind", u.GraphNode.Kind)
node.SetAttr("password_hash", string(hashedPassword))
trans := graph.NewGraphTrans(gm)
if err := trans.UpdateNode(gpart, node); err != nil {
return err
}
return trans.Commit()
}
func (u *User) CheckPassword(password string, gm *graph.Manager, gpart string) (bool, error) {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
passwordBytes := []byte(password)
n, err := gm.FetchNode(gpart, u.GraphNode.Key, "user")
if err != nil {
return false, err
}
hashedPass := n.Attr("password_hash")
if hashedPass == nil {
return false, errors.New("no password set")
}
if hashedPass.(string) == "" {
err := errors.New("no password hash found")
return false, err
}
// Comparing the password with the hash
err = bcrypt.CompareHashAndPassword([]byte(hashedPass.(string)), passwordBytes)
if err != nil {
return false, err
} else {
return true, nil
}
}
func GetUserByKey(key string, includeFavorites bool, gm *graph.Manager, gpart string) (User, error) {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
n, err := gm.FetchNode(gpart, key, "user")
if err != nil {
return User{}, err
}
if n != nil && n.Key() != "" {
user := nodeToUser(n)
if includeFavorites {
favs, err := user.GetFavorites("", gm, gpart)
if err != nil {
return User{}, err
}
user.Favorites = favs
}
return user, nil
}
return User{}, nil
}
func GetUserByEmail(email string, gm *graph.Manager, gpart string) (User, error) {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
idx, idxerr := gm.NodeIndexQuery(gpart, "user")
if idxerr == nil {
if idx == nil {
return User{}, nil
}
keys, err := idx.LookupPhrase("email", email)
if err == nil {
if len(keys) > 0 {
n, err := gm.FetchNode(gpart, keys[0], "user")
if err != nil {
return User{}, err
}
return nodeToUser(n), nil
} else {
return User{}, nil
}
}
}
return User{}, idxerr
}
func GetUserByUsername(username string, gm *graph.Manager, gpart string) (User, error) {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
idx, idxerr := gm.NodeIndexQuery(gpart, "user")
if idxerr == nil {
if idx == nil {
return User{}, nil
}
keys, err := idx.LookupPhrase("name", username)
if err == nil {
if len(keys) > 0 {
n, err := gm.FetchNode(gpart, keys[0], "user")
if err != nil {
return User{}, err
}
return nodeToUser(n), nil
} else {
return User{}, nil
}
}
}
return User{}, idxerr
}
func GetAllUsers(gm *graph.Manager, gpart string) ([]User, error) {
if !_usersInitialized {
ScaffoldUsers()
_usersInitialized = true
}
it, err := gm.NodeKeyIterator(gpart, "user")
if err != nil {
return nil, err
}
var users []User
if it == nil {
return users, nil
}
for it.HasNext() {
key := it.Next()
if it.LastError != nil {
break
}
n, err := gm.FetchNode(gpart, key, "user")
if err != nil {
return users, err
}
if n == nil {
continue
}
users = append(users, nodeToUser(n))
}
return users, nil
}
// SetFavorite connects user to a node in the graph ("favoriting" the node)
func (u *User) SetFavorite(node GraphNode, gm *graph.Manager, gpart string) (err error) {
favoriteEdge := data.NewGraphEdge()
favoriteEdge.SetAttr(data.NodeKey, getEdgeKey(u.GraphNode, "favorites", node))
favoriteEdge.SetAttr(data.NodeKind, "favoriteEdge")
favoriteEdge.SetAttr(data.NodeName, getEdgeKey(u.GraphNode, "favorites", node))
favoriteEdge.SetAttr(data.EdgeEnd1Key, u.GraphNode.Key)
favoriteEdge.SetAttr(data.EdgeEnd1Kind, "user")
favoriteEdge.SetAttr(data.EdgeEnd1Role, "favorites")
favoriteEdge.SetAttr(data.EdgeEnd1Cascading, false)
favoriteEdge.SetAttr(data.EdgeEnd2Key, node.Key)
favoriteEdge.SetAttr(data.EdgeEnd2Kind, node.Kind)
favoriteEdge.SetAttr(data.EdgeEnd2Role, "favorited")
favoriteEdge.SetAttr(data.EdgeEnd2Cascading, false)
err = gm.StoreEdge(gpart, favoriteEdge)
if err != nil {
return err
}
return nil
}
// GetFavorites returns all items that are favortied by the given user. Provide a kind to limit results, otherwise empty string will return all kinds of items.
func (u *User) GetFavorites(itemKind string, gm *graph.Manager, gpart string) ([]GraphNode, error) {
traversal := "::favorited:" + itemKind
nodes, _, err := gm.TraverseMulti(gpart, u.GraphNode.Key, u.GraphNode.Kind, traversal, false)
if err != nil {
return nil, err
}
var favoriteItems []GraphNode
for _, n := range nodes {
var gNode GraphNode
gNode.Key = n.Key()
gNode.Kind = n.Kind()
gNode.Name = n.Name()
favoriteItems = append(favoriteItems, gNode)
}
return favoriteItems, nil
}
// Get Item Favorites returns all users that have favorited a given item.
func GetItemFavorites(item GraphNode, gm *graph.Manager, gpart string) ([]User, error) {
traversal := "::favorites:user"
nodes, _, err := gm.TraverseMulti(gpart, item.Key, item.Kind, traversal, true)
if err != nil {
return nil, err
}
var users []User
for _, n := range nodes {
users = append(users, nodeToUser(n))
}
return users, nil
}
// RemoveFavorite removes a favorite from a user
func (u *User) RemoveFavorite(node GraphNode, gm *graph.Manager, gpart string) (err error) {
_, err = gm.RemoveEdge(gpart, getEdgeKey(u.GraphNode, "favorites", node), "favoriteEdge")
if err != nil {
return err
}
return nil
}
func nodeToUser(n data.Node) User {
data := n.Data()
u := User{
GraphNode: GraphNode{
Key: n.Key(),
Kind: "user",
Name: data["name"].(string),
},
UserName: data["name"].(string),
}
if data["email"] != nil {
u.Email = data["email"].(string)
}
return u
}
package data
import (
"strings"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/teris-io/shortid"
)
// A Tag is a named identifier that is used to build lists of Photos, Videos, etc..
type Tag struct {
GraphNode GraphNode
Name string `json:"name"`
}
// GetUpsertNode returns a graph node for a tag that is ready to upsert
func (t *Tag) GetUpsertNode(gm *graph.Manager, gpart string) (node data.Node, err error) {
existing, err := GetTagByKey(t.GraphNode.Key, gm, gpart)
if err != nil {
return nil, err
}
// If there is an Tag, then we need to update it, otherwise we need to create a new one
if existing.GraphNode.Key != "" {
t.GraphNode.Key = existing.GraphNode.Key
} else {
t.GraphNode.Key = shortid.MustGenerate()
}
t.GraphNode.Kind = "tag"
t.Name = strings.TrimSpace(t.Name)
t.Name = strings.ToLower(t.Name)
tagNode := data.NewGraphNode()
tagNode.SetAttr("key", t.GraphNode.Key)
tagNode.SetAttr("kind", t.GraphNode.Kind)
tagNode.SetAttr("name", t.Name)
return tagNode, nil
}
// Upsert adds or updates a Tag in the graph
func (t *Tag) Upsert(gm *graph.Manager, gpart string) error {
node, err := t.GetUpsertNode(gm, gpart)
if err != nil {
return err
}
trans := graph.NewGraphTrans(gm)
if err := trans.StoreNode(gpart, node); err != nil {
return nil
}
return trans.Commit()
}
// TagsNode connects a tag and a node in the graph ("tagging" the node)
func (t *Tag) SetTag(node GraphNode, gm *graph.Manager, gpart string) (err error) {
tagEdge := data.NewGraphEdge()
tagEdge.SetAttr(data.NodeKey, getEdgeKey(t.GraphNode, "tags", node))
tagEdge.SetAttr(data.NodeKind, "tagEdge")
tagEdge.SetAttr(data.NodeName, getEdgeKey(t.GraphNode, "tags", node))
tagEdge.SetAttr(data.EdgeEnd1Key, t.GraphNode.Key)
tagEdge.SetAttr(data.EdgeEnd1Kind, "tag")
tagEdge.SetAttr(data.EdgeEnd1Role, "tags")
tagEdge.SetAttr(data.EdgeEnd1Cascading, false)
tagEdge.SetAttr(data.EdgeEnd2Key, node.Key)
tagEdge.SetAttr(data.EdgeEnd2Kind, node.Kind)
tagEdge.SetAttr(data.EdgeEnd2Role, "tagged")
tagEdge.SetAttr(data.EdgeEnd2Cascading, false)
err = gm.StoreEdge(gpart, tagEdge)
if err != nil {
return err
}
return nil
}
// RemoveTag removes a tag from a node
func (t *Tag) RemoveTag(node GraphNode, gm *graph.Manager, gpart string) (err error) {
_, err = gm.RemoveEdge(gpart, getEdgeKey(t.GraphNode, "tags", node), "tagEdge")
if err != nil {
return err
}
// Check to see if tag is still needed. If not, remove it.
taggedItems, err := t.GetTaggedItems("", gm, gpart)
if err != nil {
return err
}
if len(taggedItems) == 0 {
_, err = gm.RemoveNode(gpart, t.GraphNode.Key, t.GraphNode.Kind)
if err != nil {
return err
}
}
return nil
}
// GetAllTags returns all tags in the graph
func GetAllTags(gm *graph.Manager, gpart string) (tags []Tag, err error) {
it, err := gm.NodeKeyIterator(gpart, "tag")
if err != nil {
return nil, err
}
if it == nil {
return tags, nil
}
for it.HasNext() {
key := it.Next()
if it.LastError != nil {
break
}
n, err := gm.FetchNode(gpart, key, "tag")
if err != nil {
return tags, err
}
tags = append(tags, nodeToTag(n))
}
return tags, nil
}
// GetTaggedItems returns all items that are tagged with the given tag. Provide a kind to limit results, otherwise empty string will return all kinds of items.
func (t *Tag) GetTaggedItems(itemKind string, gm *graph.Manager, gpart string) ([]GraphNode, error) {
traversal := "::tagged:" + itemKind
nodes, _, err := gm.TraverseMulti(gpart, t.GraphNode.Key, t.GraphNode.Kind, traversal, false)
if err != nil {
return nil, err
}
var taggedItems []GraphNode
for _, n := range nodes {
var gNode GraphNode
gNode.Key = n.Key()
gNode.Kind = n.Kind()
gNode.Name = n.Name()
taggedItems = append(taggedItems, gNode)
}
return taggedItems, nil
}
// Get Item Tags returns all tags that are assigned to the given item.
func GetItemTags(item GraphNode, gm *graph.Manager, gpart string) ([]Tag, error) {
traversal := "::tags:tag"
nodes, _, err := gm.TraverseMulti(gpart, item.Key, item.Kind, traversal, true)
if err != nil {
return nil, err
}
var tags []Tag
for _, n := range nodes {
var tag Tag
tag.GraphNode.Key = n.Key()
tag.GraphNode.Kind = n.Kind()
tag.GraphNode.Name = n.Name()
tag.Name = n.Name()
tags = append(tags, tag)
}
return tags, nil
}
// GetTagByKey returns a Tag by its key
func GetTagByKey(key string, gm *graph.Manager, gpart string) (Tag, error) {
n, err := gm.FetchNode(gpart, key, "tag")
if err != nil {
return Tag{}, err
}
if n != nil && n.Key() != "" {
return nodeToTag(n), nil
}
return Tag{}, nil
}
// GetTagsByName return a tag by its name
func GetTagByName(name string, gm *graph.Manager, gpart string) (Tag, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "tag")
if idxerr == nil {
if idx == nil {
return Tag{}, nil
}
keys, err := idx.LookupPhrase("name", name)
if err == nil {
if len(keys) > 0 {
n, err := gm.FetchNode(gpart, keys[0], "tag")
if err != nil {
return Tag{}, err
}
return nodeToTag(n), nil
} else {
return Tag{}, nil
}
}
}
return Tag{}, idxerr
}
// SearchTags searches for tags using phrase search. All tag names that contian the phrase will be included.
func SearchTags(query string, gm *graph.Manager, gpart string) ([]Tag, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "tag")
if idxerr == nil {
if idx == nil {
return []Tag{}, nil
}
keys, err := idx.LookupPhrase("name", query)
if err == nil {
var tags []Tag
for _, key := range keys {
n, err := gm.FetchNode(gpart, key, "tag")
if err != nil {
return tags, err
}
tags = append(tags, nodeToTag(n))
}
return tags, nil
}
}
return []Tag{}, idxerr
}
// Converts a tag graph node to a Tag object
func nodeToTag(n data.Node) Tag {
data := n.Data()
t := Tag{
GraphNode: GraphNode{
Key: n.Key(),
Kind: "tag",
Name: data["name"].(string),
},
Name: data["name"].(string),
}
return t
}
package data
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"webster/MemoryLane/graphStore"
)
// Search takes a search string and returns a list of GraphNodes
func Search(searchString string, wordsIn []string, user User, resultsIn [][]GraphNode) ([]GraphNode, error) {
var words []string
if wordsIn != nil {
words = wordsIn
} else {
words = strings.Split(searchString, " ")
}
deepestStart := 0
deepestEnd := len(words) - 1
deepestLevel := 0
depthCurrent := 0
for i, word := range words {
if strings.HasPrefix(word, "(") {
depthCurrent++
if depthCurrent >= deepestLevel {
deepestLevel = depthCurrent
deepestStart = i
deepestEnd = i
}
} else if strings.HasSuffix(word, ")") {
if depthCurrent == deepestLevel {
deepestEnd = i
}
depthCurrent--
}
}
var searchType string
var searchTerms []string
var command string
re := regexp.MustCompile(`:\d:`)
results := []GraphNode{}
var err error
// Calculate the depth of the deepest parenthetical group
for x := deepestStart; x <= deepestEnd; x++ {
word := strings.Replace(words[x], "(", "", -1)
word = strings.Replace(word, ")", "", -1)
switch word {
case "":
continue
case "Is:", "is:", "IsA:", "isa:":
searchType = "is"
case "Tag:", "tag:":
searchType = "tag"
case "Caption:", "caption:", "Cap:", "cap:":
searchType = "caption"
case "Month:", "M:", "month:", "m:":
searchType = "month"
case "Year:", "Y:", "year:", "y:":
searchType = "year"
case "and", "AND", "And":
results, err = searchIfReady(searchType, searchTerms, command, user, results)
if err != nil {
return nil, err
}
searchTerms = []string{}
command = "and"
case "or", "OR", "Or":
results, err = searchIfReady(searchType, searchTerms, command, user, results)
if err != nil {
return nil, err
}
searchTerms = []string{}
command = "or"
case "not", "NOT", "Not":
results, err = searchIfReady(searchType, searchTerms, command, user, results)
if err != nil {
return nil, err
}
searchTerms = []string{}
command = "not"
default:
rInString := re.FindString(word)
if rInString != "" {
rInString = strings.ReplaceAll(rInString, ":", "")
rInInt, err := strconv.Atoi(rInString)
if err != nil {
return results, err
}
rIn := resultsIn[rInInt]
results = processCmd(results, rIn, command)
} else {
searchTerms = append(searchTerms, word)
}
}
words[x] = ""
}
results, err = searchIfReady(searchType, searchTerms, command, user, results)
if err != nil {
return nil, err
}
for _, word := range words {
if word != "" {
newWords := words
rout := append(resultsIn, results)
copy(newWords[deepestEnd:], []string{fmt.Sprintf(":%d:", len(rout)-1)})
return Search("", newWords, user, rout)
}
}
results = deDuplicate(results)
return results, nil
//check for end
}
func deDuplicate(items []GraphNode) []GraphNode {
results := []GraphNode{}
// go through the items add them to results only if they are not already in the list.
for _, item := range items {
skip := false
for _, result := range results {
if result.Key == item.Key {
skip = true
break
}
}
if !skip {
results = append(results, item)
}
}
return results
}
func runSearch(searchType string, searchTerms []string, user User) ([]GraphNode, error) {
// Run the code for the deepest parenthetical group
results := []GraphNode{}
gm := graphStore.GetGraphManager()
gpart := graphStore.GetGpart()
var err error
switch searchType {
case "is":
if len(searchTerms) != 1 {
return nil, errors.New("'Is' search requires exactly one search term")
}
switch searchTerms[0] {
case "favorite", "fav":
favorites, err := user.GetFavorites("", gm, gpart)
if err != nil {
return nil, err
}
results = favorites
}
case "tag":
if len(searchTerms) == 0 {
return nil, errors.New("Tag search requires at least one search term")
}
tags, err := SearchTags(strings.Join(searchTerms, " "), gm, gpart)
if err != nil {
return nil, err
}
// Go through the tags and get tagged items
for _, tag := range tags {
items, err := tag.GetTaggedItems("", gm, gpart)
if err != nil {
return nil, err
}
results = append(results, items...)
}
case "caption":
if len(searchTerms) == 0 {
return nil, errors.New("Caption search requires at least one search term")
}
captions, err := SearchCaptions(strings.Join(searchTerms, " "), gm, gpart)
if err != nil {
return nil, err
}
// Go through the captions and get captioned items
for _, caption := range captions {
item := caption.CaptionedItem
if err != nil {
return nil, err
}
results = append(results, item)
}
case "month":
if len(searchTerms) == 0 {
return nil, errors.New("Month search requires at least one search term")
}
searchMonths := []int{}
for _, term := range searchTerms {
searchMonth := MonthStringToInt(term)
if searchMonth == 0 {
return nil, errors.New("Month search requires a valid month")
}
searchMonths = append(searchMonths, searchMonth)
}
results, err = FindTakenInMonths(searchMonths)
if err != nil {
return nil, err
}
case "year":
if len(searchTerms) == 0 {
return nil, errors.New("Year search requires at least one search term")
}
searchYears := []int{}
for _, term := range searchTerms {
searchTerm, err := strconv.Atoi(term)
if err != nil || searchTerm < 1900 || searchTerm > 2100 {
return nil, errors.New("Year search requires a valid year")
}
searchYears = append(searchYears, searchTerm)
}
results, err = FindTakenInYears(searchYears)
if err != nil {
return nil, err
}
}
return results, nil
}
func searchIfReady(searchType string, searchTerms []string, command string, user User, results []GraphNode) ([]GraphNode, error) {
if searchType != "" && len(searchTerms) > 0 {
r, err := runSearch(searchType, searchTerms, user)
if err != nil {
return nil, err
}
return processCmd(results, r, command), nil
}
return results, nil
}
func processCmd(listA []GraphNode, listB []GraphNode, cmd string) []GraphNode {
switch cmd {
case "and":
return processAnd(listA, listB)
case "or":
return processOr(listA, listB)
case "not":
return processNot(listA, listB)
default:
return processOr(listA, listB)
}
}
func processAnd(listA []GraphNode, listB []GraphNode) []GraphNode {
results := []GraphNode{}
for _, a := range listA {
for _, b := range listB {
if a.Key == b.Key {
results = append(results, a)
}
}
}
return results
}
func processOr(listA []GraphNode, listB []GraphNode) []GraphNode {
results := []GraphNode{}
for _, a := range listA {
results = append(results, a)
}
for _, b := range listB {
results = append(results, b)
}
return results
}
func processNot(listA []GraphNode, listB []GraphNode) []GraphNode {
results := []GraphNode{}
for _, a := range listA {
found := false
for _, b := range listB {
if a.Key == b.Key {
found = true
}
}
if !found {
results = append(results, a)
}
}
return results
}
package data
import (
"errors"
"time"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/teris-io/shortid"
)
type Photo struct {
GraphNode GraphNode
BlurHash string `json:"blurHash"`
Make string `json:"make"`
Model string `json:"model"`
LensInfo string `json:"lensInfo"`
LensMake string `json:"lensMake"`
DateTaken time.Time `json:"date_taken"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
Altitude int `json:"altitude"`
ProcessedPreviews string `json:"processed_previews,omitempty"`
ProcessedPeople string `json:"processed_people,omitempty"`
Tags []Tag `json:"tags,omitempty"`
FavoritedBy []User `json:"favorited_by,omitempty"`
Caption Caption `json:"caption,omitempty"`
}
type PhotoProcessing string
const (
Previews PhotoProcessing = "Previews"
Blurhash PhotoProcessing = "Blurhash"
People PhotoProcessing = "People"
)
func (p *Photo) GetUpsertNode(gm *graph.Manager, gpart string) (node data.Node, err error) {
existing, err := GetPhotoByKey(p.GraphNode.Key, false, false, false, gm, gpart)
if err != nil {
return nil, err
}
// If there is an Photo, then we need to update it, otherwise we need to create a new one
if existing.GraphNode.Key != "" {
p.GraphNode.Key = existing.GraphNode.Key
} else {
p.GraphNode.Key = shortid.MustGenerate()
p.BlurHash = "false"
p.ProcessedPreviews = "false"
p.ProcessedPeople = "false"
}
p.GraphNode.Kind = "photo"
photoNode := data.NewGraphNode()
photoNode.SetAttr("key", p.GraphNode.Key)
photoNode.SetAttr("kind", p.GraphNode.Kind)
photoNode.SetAttr("name", p.GraphNode.Key)
photoNode.SetAttr("blurHash", p.BlurHash)
photoNode.SetAttr("processed_previews", p.ProcessedPreviews)
photoNode.SetAttr("processed_people", p.ProcessedPeople)
if p.Make != "" {
photoNode.SetAttr("make", p.Make)
}
if p.Model != "" {
photoNode.SetAttr("model", p.Model)
}
if p.LensInfo != "" {
photoNode.SetAttr("lensInfo", p.LensInfo)
}
if p.LensMake != "" {
photoNode.SetAttr("lensMake", p.LensMake)
}
if p.DateTaken.Unix() != 0 {
timeString := p.DateTaken.Unix()
photoNode.SetAttr("date_taken", timeString)
}
if p.Longitude != 0 {
photoNode.SetAttr("longitude", p.Longitude)
}
if p.Latitude != 0 {
photoNode.SetAttr("latitude", p.Latitude)
}
if p.Altitude != 0 {
photoNode.SetAttr("altitude", p.Altitude)
}
return photoNode, nil
}
func (p *Photo) Upsert(gm *graph.Manager, gpart string) error {
node, err := p.GetUpsertNode(gm, gpart)
if err != nil {
return err
}
trans := graph.NewGraphTrans(gm)
if err := trans.StoreNode(gpart, node); err != nil {
return nil
}
return trans.Commit()
}
func (p *Photo) GetAsset(gm *graph.Manager, gpart string) (asset Asset, err error) {
node, _, err := gm.TraverseMulti(gpart, p.GraphNode.Key, p.GraphNode.Kind, ":::asset", true)
if err != nil {
return Asset{}, err
}
return nodeToAsset(node[0]), nil
}
func GetPhotoByKey(key string, includeTags bool, includeFavoritedBy bool, includeCaption bool, gm *graph.Manager, gpart string) (Photo, error) {
n, err := gm.FetchNode(gpart, key, "photo")
if err != nil {
return Photo{}, err
}
if n != nil && n.Key() != "" {
photo := nodeToPhoto(n)
if includeTags {
tags, err := GetItemTags(photo.GraphNode, gm, gpart)
if err != nil {
return photo, err
}
photo.Tags = tags
}
if includeFavoritedBy {
users, err := GetItemFavorites(photo.GraphNode, gm, gpart)
if err != nil {
return photo, err
}
photo.FavoritedBy = users
}
if includeCaption {
caption, err := GetItemCaption(photo.GraphNode, gm, gpart)
if err != nil {
return photo, err
}
photo.Caption = caption
}
return photo, nil
}
return Photo{}, nil
}
func GetUnprocessedPhotos(p PhotoProcessing, gm *graph.Manager, gpart string) ([]Photo, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "photo")
var photosToProcess []Photo
if idxerr == nil && idx != nil {
var keys []string
var err error
switch p {
case Previews:
keys, err = idx.LookupValue("processed_previews", "false")
case Blurhash:
keys, err = idx.LookupValue("blurHash", "false")
for _, key := range keys {
photo, err := GetPhotoByKey(key, false, false, false, gm, gpart)
if err != nil {
return nil, err
}
if photo.ProcessedPreviews == "true" {
photosToProcess = append(photosToProcess, photo)
}
}
return photosToProcess, nil
case People:
keys, err = idx.LookupValue("processed_people", "false")
default:
return nil, errors.New("Invalid processing type")
}
if err != nil {
return nil, err
}
for _, key := range keys {
photo, err := GetPhotoByKey(key, false, false, false, gm, gpart)
if err != nil {
return nil, err
}
photosToProcess = append(photosToProcess, photo)
}
return photosToProcess, nil
}
return nil, nil
}
func nodeToPhoto(n data.Node) Photo {
data := n.Data()
p := Photo{
GraphNode: GraphNode{
Key: n.Key(),
Kind: "photo",
Name: data["name"].(string),
},
}
if data["blurHash"] != nil {
p.BlurHash = data["blurHash"].(string)
}
if data["make"] != nil {
p.Make = data["make"].(string)
}
if data["model"] != nil {
p.Model = data["model"].(string)
}
if data["lensInfo"] != nil {
p.LensInfo = data["lensInfo"].(string)
}
if data["lensMake"] != nil {
p.LensMake = data["lensMake"].(string)
}
if data["date_taken"] != nil {
tm := time.Unix(data["date_taken"].(int64), 0)
p.DateTaken = tm
}
if data["longitude"] != nil {
p.Longitude = data["longitude"].(float64)
}
if data["latitude"] != nil {
p.Latitude = data["latitude"].(float64)
}
if data["altitude"] != nil {
p.Altitude = data["altitude"].(int)
}
if data["processed_previews"] != nil {
p.ProcessedPreviews = data["processed_previews"].(string)
}
if data["processed_people"] != nil {
p.ProcessedPeople = data["processed_people"].(string)
}
return p
}
<svg id="mermaid-1640117954925" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="1304.66650390625" style="max-width: 1412.4354248046875px;" viewBox="0 0 1412.4354248046875 1304.66650390625"><style>#mermaid-1640117954925 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-1640117954925 .error-icon{fill:#552222;}#mermaid-1640117954925 .error-text{fill:#552222;stroke:#552222;}#mermaid-1640117954925 .edge-thickness-normal{stroke-width:2px;}#mermaid-1640117954925 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-1640117954925 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-1640117954925 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-1640117954925 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-1640117954925 .marker{fill:#333333;stroke:#333333;}#mermaid-1640117954925 .marker.cross{stroke:#333333;}#mermaid-1640117954925 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-1640117954925 g.classGroup text{fill:#9370DB;fill:#131300;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-1640117954925 g.classGroup text .title{font-weight:bolder;}#mermaid-1640117954925 .nodeLabel,#mermaid-1640117954925 .edgeLabel{color:#131300;}#mermaid-1640117954925 .edgeLabel .label rect{fill:#ECECFF;}#mermaid-1640117954925 .label text{fill:#131300;}#mermaid-1640117954925 .edgeLabel .label span{background:#ECECFF;}#mermaid-1640117954925 .classTitle{font-weight:bolder;}#mermaid-1640117954925 .node rect,#mermaid-1640117954925 .node circle,#mermaid-1640117954925 .node ellipse,#mermaid-1640117954925 .node polygon,#mermaid-1640117954925 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-1640117954925 .divider{stroke:#9370DB;stroke:1;}#mermaid-1640117954925 g.clickable{cursor:pointer;}#mermaid-1640117954925 g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-1640117954925 g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-1640117954925 .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-1640117954925 .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-1640117954925 .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-1640117954925 .dashed-line{stroke-dasharray:3;}#mermaid-1640117954925 #compositionStart,#mermaid-1640117954925 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #compositionEnd,#mermaid-1640117954925 .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #dependencyStart,#mermaid-1640117954925 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #dependencyStart,#mermaid-1640117954925 .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #extensionStart,#mermaid-1640117954925 .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #extensionEnd,#mermaid-1640117954925 .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #aggregationStart,#mermaid-1640117954925 .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 #aggregationEnd,#mermaid-1640117954925 .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-1640117954925 .edgeTerminals{font-size:11px;}#mermaid-1640117954925 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g transform="translate(0, -0.00000762939453125)"><defs><marker id="classDiagram-aggregationStart" class="marker aggregation classDiagram" refX="0" refY="7" markerWidth="190" markerHeight="240" orient="auto"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker id="classDiagram-aggregationEnd" class="marker aggregation classDiagram" refX="19" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker id="classDiagram-extensionStart" class="marker extension classDiagram" refX="0" refY="7" markerWidth="190" markerHeight="240" orient="auto"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker id="classDiagram-extensionEnd" class="marker extension classDiagram" refX="19" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker id="classDiagram-compositionStart" class="marker composition classDiagram" refX="0" refY="7" markerWidth="190" markerHeight="240" orient="auto"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker id="classDiagram-compositionEnd" class="marker composition classDiagram" refX="19" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker id="classDiagram-dependencyStart" class="marker dependency classDiagram" refX="0" refY="7" markerWidth="190" markerHeight="240" orient="auto"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker id="classDiagram-dependencyEnd" class="marker dependency classDiagram" refX="19" refY="7" markerWidth="20" markerHeight="28" orient="auto"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M108.12369537353516,221.66665649414062L108.12369537353516,227.36110083262125C108.12369537353516,233.0555451711019,108.12369537353516,244.44443384806314,210.92588504155478,278.2445532905063C313.7280747095744,312.04467273294944,519.3324540456136,368.2560229408744,622.1346437136332,396.36169804483694L724.9368333816528,424.4673731487995" id="id1" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M181.21348565166727,221.66665649414062L185.10932742014143,227.36110083262125C189.00516918861558,233.0555451711019,196.7968527255639,244.44443384806314,200.69269449403805,282.94443194071454C204.5885362625122,321.4444300333659,204.5885362625122,387.05553754170734,204.5885362625122,452.6666450500488C204.5885362625122,518.2777525583903,204.5885362625122,583.8888600667318,204.5885362625122,630.8888581593832C204.5885362625122,677.8888562520345,204.5885362625122,706.2777449289957,204.5885362625122,734.666633605957C204.5885362625122,763.0555222829183,204.5885362625122,791.4444109598795,204.5885362625122,823.5555210113525C204.5885362625122,855.6666310628256,204.5885362625122,891.4999624888102,204.5885362625122,927.3332939147949C204.5885362625122,963.1666253407797,204.5885362625122,998.9999567667643,276.42393080393475,1027.8764121978372C348.25932534535724,1056.75286762891,491.93011442820233,1078.6724470650709,563.7655089696249,1089.6322367831513L635.6009035110474,1100.5920265012317" id="id2" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M724.9368333816528,500.6122568336479L671.5535875956217,525.4268752905522C618.1703418095907,550.2414937474564,511.4038502375285,599.8707306612648,458.0206044514974,638.8797934566496C404.6373586654663,677.8888562520345,404.6373586654663,706.2777449289957,404.6373586654663,734.666633605957C404.6373586654663,763.0555222829183,404.6373586654663,791.4444109598795,404.6373586654663,823.5555210113525C404.6373586654663,855.6666310628256,404.6373586654663,891.4999624888102,404.6373586654663,927.3332939147949C404.6373586654663,963.1666253407797,404.6373586654663,998.9999567667643,404.6373586654663,1029.2499554951985C404.6373586654663,1059.4999542236328,404.6373586654663,1084.1666202545166,404.6373586654663,1108.8332862854004C404.6373586654663,1133.4999523162842,404.6373586654663,1158.166618347168,410.27423962989997,1176.1943957010906C415.9111205943337,1194.222173055013,427.1848825232011,1205.6110617319744,432.82176348763477,1211.3055060704548L438.4586444520684,1216.9999504089355" id="id3" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M724.9368333816528,518.5118847670634L690.7387008666992,540.3432319017317C656.5405683517456,562.1745790364,588.1443033218384,605.8372733057366,553.9461708068848,641.8630647788856C519.7480382919312,677.8888562520345,519.7480382919312,706.2777449289957,519.7480382919312,734.666633605957C519.7480382919312,763.0555222829183,519.7480382919312,791.4444109598795,519.7480382919312,823.5555210113525C519.7480382919312,855.6666310628256,519.7480382919312,891.4999624888102,519.7480382919312,927.3332939147949C519.7480382919312,963.1666253407797,519.7480382919312,998.9999567667643,519.7480382919312,1029.2499554951985C519.7480382919312,1059.4999542236328,519.7480382919312,1084.1666202545166,519.7480382919312,1108.8332862854004C519.7480382919312,1133.4999523162842,519.7480382919312,1158.166618347168,529.2527046203613,1177.1406268666428C538.7573709487915,1196.1146353861177,557.7667036056519,1209.3959863941836,567.271369934082,1216.0366618982166L576.7760362625122,1222.6773374022496" id="id4" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M828.0813722610474,615.3333015441895L828.0813722610474,621.02774588267C828.0813722610474,626.7221902211508,828.0813722610474,638.1110788981119,828.0813722610474,657.9999675750732C828.0813722610474,677.8888562520345,828.0813722610474,706.2777449289957,828.0813722610474,734.666633605957C828.0813722610474,763.0555222829183,828.0813722610474,791.4444109598795,828.0813722610474,823.5555210113525C828.0813722610474,855.6666310628256,828.0813722610474,891.4999624888102,828.0813722610474,927.3332939147949C828.0813722610474,963.1666253407797,828.0813722610474,998.9999567667643,828.0813722610474,1029.2499554951985C828.0813722610474,1059.4999542236328,828.0813722610474,1084.1666202545166,828.0813722610474,1108.8332862854004C828.0813722610474,1133.4999523162842,828.0813722610474,1158.166618347168,826.2156881797958,1176.1943957010906C824.3500040985442,1194.222173055013,820.618635936041,1205.6110617319744,818.7529518547894,1211.3055060704548L816.8872677735378,1216.9999504089355" id="id5" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M902.424043527009,615.3333015441895L905.0265449407713,621.02774588267C907.6290463545334,626.7221902211508,912.834049182058,638.1110788981119,915.4365505958203,649.4999675750732C918.0390520095825,660.8888562520345,918.0390520095825,672.2777449289957,918.0390520095825,677.9721892674764L918.0390520095825,683.666633605957" id="id6" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M635.6009035110474,1142.5239121870413L624.8294188181559,1149.2421408855432C614.0579341252645,1155.9603695840449,592.5149647394816,1169.3968269810482,574.3774868647257,1181.9709621888142C556.2400089899699,1194.54509739658,541.5080226262411,1206.2569104151082,534.1420294443766,1212.1128169243723L526.7760362625122,1217.9687234336366" id="id7" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M687.7148971545719,1148.666618347168L687.4428601255039,1154.3610626856487C687.1708230964358,1160.0555070241292,686.6267490382997,1171.4443957010906,681.7052814911675,1182.8332843780518C676.7838139440352,1194.222173055013,667.4849529079069,1205.6110617319744,662.8355223898427,1211.3055060704548L658.1860918717786,1216.9999504089355" id="id8" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M731.4017562058972,1148.666618347168L737.3750512721975,1154.3610626856487C743.3483463384977,1160.0555070241292,755.2949364710981,1171.4443957010906,764.084292369488,1182.8332843780518C772.8736482678778,1194.222173055013,778.5057699320569,1205.6110617319744,781.3218307641465,1211.3055060704548L784.137891596236,1216.9999504089355" id="id9" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M743.6347551345825,1092.8559267136734L776.3289825121561,1083.185486960186C809.0232098897299,1073.5150472066987,874.4116646448771,1054.1741676997237,907.1058920224508,1026.587062233244C939.8001194000244,998.9999567667643,939.8001194000244,963.1666253407797,939.8001194000244,927.3332939147949C939.8001194000244,891.4999624888102,939.8001194000244,855.6666310628256,938.3451230643001,832.0555210113525C936.8901267285756,808.4444109598795,933.980134057127,797.0555222829183,932.5251377214026,791.3610779444376L931.0701413856782,785.666633605957" id="id10" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M903.8539761064751,785.666633605957L902.270130514958,791.3610779444376C900.6862849234411,797.0555222829183,897.518593740407,808.4444109598795,916.1702074765877,824.6278803746153C934.8218212127686,840.811349789351,975.2927398681641,861.7893999418611,995.5281991958618,872.2784250181162L1015.7636585235596,882.7674500943713" id="id11" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1178.2245979309082,138.16851173669588L1119.8673936525981,157.77931353475063C1061.5101893742878,177.3901153328054,944.7957808176676,216.6117189289149,886.4385765393575,241.91696506545028C828.0813722610474,267.22221120198566,828.0813722610474,278.61109987894696,828.0813722610474,284.30554421742755L828.0813722610474,289.9999885559082" id="id12" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1236.6652046649842,176.99999237060547L1234.3404678080801,190.1388807296753C1232.015730951176,203.27776908874512,1227.3662572373678,229.55554580688477,1225.0415203804637,275.4999879201253C1222.7167835235596,321.4444300333659,1222.7167835235596,387.05553754170734,1222.7167835235596,452.6666450500488C1222.7167835235596,518.2777525583903,1222.7167835235596,583.8888600667318,1222.7167835235596,630.8888581593832C1222.7167835235596,677.8888562520345,1222.7167835235596,706.2777449289957,1222.7167835235596,734.666633605957C1222.7167835235596,763.0555222829183,1222.7167835235596,791.4444109598795,1222.7167835235596,823.5555210113525C1222.7167835235596,855.6666310628256,1222.7167835235596,891.4999624888102,1222.7167835235596,927.3332939147949C1222.7167835235596,963.1666253407797,1222.7167835235596,998.9999567667643,1142.8697787920635,1028.0002649278897C1063.0227740605671,1057.000573089015,903.3287645975748,1079.1678579852808,823.4817598660787,1090.2515004334136L743.6347551345825,1101.3351428815465" id="id13" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1258.6641944439025,176.99999237060547L1260.9889313008066,190.1388807296753C1263.3136681577107,203.27776908874512,1267.963141871519,229.55554580688477,1270.287878728423,275.4999879201253C1272.6126155853271,321.4444300333659,1272.6126155853271,387.05553754170734,1272.6126155853271,452.6666450500488C1272.6126155853271,518.2777525583903,1272.6126155853271,583.8888600667318,1272.6126155853271,630.8888581593832C1272.6126155853271,677.8888562520345,1272.6126155853271,706.2777449289957,1272.6126155853271,734.666633605957C1272.6126155853271,763.0555222829183,1272.6126155853271,791.4444109598795,1258.4633102416992,814.5405286633178C1244.3140048980713,837.6366463667563,1216.0153942108154,855.4399930966716,1201.8660888671875,864.3416664616293L1187.7167835235596,873.243339826587" id="id14" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1317.1048011779785,75.66642057258792L1323.9009526570637,71.83312783107912C1330.6971041361492,67.99983508957034,1344.2894070943196,60.33324960655275,1352.7845964431763,56.49995686504395C1361.279785792033,52.666664123535156,1364.6778615315754,52.666664123535156,1368.0759372711182,63.027774810791016C1371.4740130106609,73.38888549804688,1374.8720887502034,94.1111068725586,1374.8720887502034,114.83332824707031C1374.8720887502034,135.55554962158203,1371.4740130106609,156.27777099609375,1368.0759372711182,166.6388816833496C1364.6778615315754,176.99999237060547,1361.279785792033,176.99999237060547,1352.7845964431763,173.16669962909668C1344.2894070943196,169.33340688758787,1330.6971041361492,161.6668214045703,1323.9009526570637,157.83352866306151L1317.1048011779785,154.0002359215527" id="id15" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1276.1451249198751,176.99999237060547L1282.1644462807837,190.1388807296753C1288.1837676416924,203.27776908874512,1300.2224103635099,229.55554580688477,1242.7358747336043,268.5114383203226C1185.2493391036987,307.4673308337604,1058.2376251220703,359.10133914249644,994.7317681312561,384.9183432968645L931.2259111404419,410.7353474512325" id="id16" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M291.26835864582097,165.8333282470703L284.4248029672308,180.833327293396C277.58124728864055,195.83332633972168,263.8941359314602,225.83332443237305,336.17221505409884,267.7834283308683C408.4502941767375,309.73353222936356,566.6935637791952,363.63374193370265,645.815198580424,390.58384678587214L724.9368333816528,417.53395163804174" id="id17" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M328.59832632031856,165.8333282470703L332.7341722008819,180.833327293396C336.87001808144527,195.83332633972168,345.141709842572,225.83332443237305,349.27755572313544,273.63887723286945C353.41340160369873,321.4444300333659,353.41340160369873,387.05553754170734,353.41340160369873,452.6666450500488C353.41340160369873,518.2777525583903,353.41340160369873,583.8888600667318,353.41340160369873,630.8888581593832C353.41340160369873,677.8888562520345,353.41340160369873,706.2777449289957,353.41340160369873,734.666633605957C353.41340160369873,763.0555222829183,353.41340160369873,791.4444109598795,353.41340160369873,823.5555210113525C353.41340160369873,855.6666310628256,353.41340160369873,891.4999624888102,353.41340160369873,927.3332939147949C353.41340160369873,963.1666253407797,353.41340160369873,998.9999567667643,400.44465192159015,1027.2683970159524C447.4759022394816,1055.5368372651403,541.5384028752645,1076.2403863375316,588.5696531931559,1086.5921608737274L635.6009035110474,1096.943935409923" id="id18" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1101.7402210235596,853.9999656677246L1101.7402210235596,848.305521329244C1101.7402210235596,842.6110769907633,1101.7402210235596,831.2221883138021,1101.7402210235596,811.3332996368408C1101.7402210235596,791.4444109598795,1101.7402210235596,763.0555222829183,1101.7402210235596,734.666633605957C1101.7402210235596,706.2777449289957,1101.7402210235596,677.8888562520345,1073.3211693763733,643.253573314615C1044.902117729187,608.6182903771954,988.0640144348145,567.7366131793176,959.6449627876282,547.2957745803786L931.2259111404419,526.8549359814398" id="id19" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/><path d="M1101.7402210235596,1000.6666221618652L1101.7402210235596,1006.3610665003458C1101.7402210235596,1012.0555108388265,1101.7402210235596,1023.4443995157877,1042.05597670873,1039.8556455966639C982.3717323939005,1056.26689167754,863.0032437642416,1077.700495162331,803.318999449412,1088.4172969047265L743.6347551345825,1099.1340986471218" id="id20" class=" edge-pattern-solid relation" style="fill:none" marker-start="url(#classDiagram-extensionStart)" marker-end="url(#classDiagram-extensionEnd)"/></g><g class="edgeLabels"><g class="edgeLabel" transform="translate(108.12369537353516, 255.8333225250244)"><g class="label" transform="translate(-12.135416030883789, -9.166666030883789)"><foreignObject width="24.270832061767578" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">is a</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(93.52970910490843, 239.21042339496643)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(707.0120959947592, 400.383317932996)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(204.5885362625122, 734.666633605957)"><g class="label" transform="translate(-12.135416030883789, -9.166666030883789)"><foreignObject width="24.270832061767578" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">is a</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(178.71486682175072, 244.57968562480747)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(615.5634320594037, 1078.1242146078948)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(404.6373586654663, 927.3332939147949)"><g class="label" transform="translate(-31.223957061767578, -9.166666030883789)"><foreignObject width="62.447914123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken on</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(702.7446710849051, 494.386669905911)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(431.80764262172477, 1189.010296367254)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(519.7480382919312, 927.3332939147949)"><g class="label" transform="translate(-31.223957061767578, -9.166666030883789)"><foreignObject width="62.447914123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken on</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(702.1149783907532, 515.2849768392862)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(566.0215551409203, 1195.3583780398321)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(828.0813722610474, 927.3332939147949)"><g class="label" transform="translate(-29.934894561767578, -9.166666030883789)"><foreignObject width="59.869789123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken at</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(813.0813722610474, 632.8333015441895)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(831.590293670637, 1200.0399850871534)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(918.0390520095825, 649.4999675750732)"><g class="label" transform="translate(-40.963539123535156, -9.166666030883789)"><foreignObject width="81.92707824707031" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">has caption</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(896.055556160913, 637.4848786588752)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(927.8651179581252, 661.0555684822331)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(570.9719953536987, 1182.8332843780518)"><g class="label" transform="translate(-31.223957061767578, -9.166666030883789)"><foreignObject width="62.447914123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken on</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(612.8141689899787, 1139.0576851508622)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(544.8091679392821, 1213.8201018222378)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(686.0826749801636, 1182.8332843780518)"><g class="label" transform="translate(-31.223957061767578, -9.166666030883789)"><foreignObject width="62.447914123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken on</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(671.6757555813424, 1165.1387676985892)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(675.8729863634954, 1207.931189132858)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(767.2415266036987, 1182.8332843780518)"><g class="label" transform="translate(-29.934894561767578, -9.166666030883789)"><foreignObject width="59.869789123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">taken at</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(733.7180906905605, 1171.598792133252)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(784.8261118490916, 1189.6640118713456)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(939.8001194000244, 927.3332939147949)"><g class="label" transform="translate(-40.963539123535156, -9.166666030883789)"><foreignObject width="81.92707824707031" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">has caption</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(764.6706150088409, 1102.2762557605042)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(915.8693156082734, 801.3352850728292)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(894.350902557373, 819.8332996368408)"><g class="label" transform="translate(-23.52213478088379, -9.166666030883789)"><foreignObject width="47.04426956176758" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">author</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(884.7131362163786, 798.5071261316235)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1002.1298521229477, 856.3967367923601)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(828.0813722610474, 255.8333225250244)"><g class="label" transform="translate(-29.895832061767578, -9.166666030883789)"><foreignObject width="59.791664123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">contians</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1156.8580606298137, 129.52437593483896)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(838.4723886903658, 268.1279613859373)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1222.7167835235596, 734.666633605957)"><g class="label" transform="translate(-29.895832061767578, -9.166666030883789)"><foreignObject width="59.791664123535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">contians</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1218.8456142577072, 191.6188910198498)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(758.0309440384067, 1108.7865627205515)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1272.6126155853271, 649.4999675750732)"><g class="label" transform="translate(-23.52213478088379, -9.166666030883789)"><foreignObject width="47.04426956176758" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">author</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1246.9426319587035, 196.8457720959167)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1205.5168365236018, 871.620851217428)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1378.270164489746, 114.83332824707031)"><g class="label" transform="translate(-26.165363311767578, -9.166666030883789)"><foreignObject width="52.330726623535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">links to</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1339.7165258891246, 80.13407158708577)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1319.9781553716575, 170.66265253976212)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1312.2610530853271, 255.8333225250244)"><g class="label" transform="translate(-19.6484375, -9.166666030883789)"><foreignObject width="39.296875" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">cover</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1269.7968898262811, 199.15738190990956)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(948.0864849409675, 413.0405040622692)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel" transform="translate(250.20702457427979, 255.8333225250244)"><g class="label" transform="translate(-25.618488311767578, -9.166666030883789)"><foreignObject width="51.236976623535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">has tag</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(270.35770599962046, 175.5284097789579)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(708.2078055608523, 392.69258307690444)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g><g class="edgeLabel" transform="translate(353.41340160369873, 734.666633605957)"><g class="label" transform="translate(-25.618488311767578, -9.166666030883789)"><foreignObject width="51.236976623535156" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">has tag</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(318.7895003057485, 186.69086722500091)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(616.7343814045003, 1073.5328092990997)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1101.7402210235596, 734.666633605957)"><g class="label" transform="translate(-28.30078125, -9.166666030883789)"><foreignObject width="56.6015625" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">favorite</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1116.7402210235596, 836.4999656677246)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(931.6740316720966, 544.2506494702429)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g><g class="edgeLabel" transform="translate(1101.7402210235596, 1034.833288192749)"><g class="label" transform="translate(-28.30078125, -9.166666030883789)"><foreignObject width="56.6015625" height="18.333332061767578"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel"><span class="edgeLabel">favorite</span></span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(1086.3343962036713, 1017.4651463980209)"><g class="inner" transform="translate(0, 0)"><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="edgeTerminals" transform="translate(758.5102664391809, 1105.8051761629633)"><g class="inner" transform="translate(0, 0)"/><foreignObject style="width: 9px; height: 12px;"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="edgeLabel">∞</span></div></foreignObject></g></g><g class="nodes"><g class="node default" id="classid-Memory-174" transform="translate(1247.6646995544434, 114.83332824707031)"><rect class="outer title-state" x="-69.44010162353516" y="-62.166664123535156" width="138.8802032470703" height="124.33332824707031"/><line class="divider" x1="-69.44010162353516" x2="69.44010162353516" y1="-31.833332061767578" y2="-31.833332061767578"/><line class="divider" x1="-69.44010162353516" x2="69.44010162353516" y1="51.166664123535156" y2="51.166664123535156"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="59.296875" height="18.333332061767578" transform="translate( -29.6484375, -54.666664123535156)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Memory</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -61.940101623535156, -20.333332061767578)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject><foreignObject width="97.578125" height="18.333332061767578" transform="translate( -61.940101623535156, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Name: string</span></div></foreignObject><foreignObject width="123.88020324707031" height="18.333332061767578" transform="translate( -61.940101623535156, 24.333332061767578)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Narrative: string</span></div></foreignObject></g></g><g class="node default" id="classid-User-175" transform="translate(1101.7402210235596, 927.3332939147949)"><rect class="outer title-state" x="-85.9765625" y="-73.33333015441895" width="171.953125" height="146.6666603088379"/><line class="divider" x1="-85.9765625" x2="85.9765625" y1="-42.99999809265137" y2="-42.99999809265137"/><line class="divider" x1="-85.9765625" x2="85.9765625" y1="62.333330154418945" y2="62.333330154418945"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="33.776039123535156" height="18.333332061767578" transform="translate( -16.888019561767578, -65.83333015441895)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">User</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -78.4765625, -31.499998092651367)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject><foreignObject width="97.578125" height="18.333332061767578" transform="translate( -78.4765625, -9.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Name: string</span></div></foreignObject><foreignObject width="96.484375" height="18.333332061767578" transform="translate( -78.4765625, 13.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Email: string</span></div></foreignObject><foreignObject width="156.953125" height="18.333332061767578" transform="translate( -78.4765625, 35.49999809265137)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-PasswordHash: string</span></div></foreignObject></g></g><g class="node default" id="classid-Asset-176" transform="translate(108.12369537353516, 114.83332824707031)"><rect class="outer title-state" x="-100.12369537353516" y="-106.83332824707031" width="200.2473907470703" height="213.66665649414062"/><line class="divider" x1="-100.12369537353516" x2="100.12369537353516" y1="-76.49999618530273" y2="-76.49999618530273"/><line class="divider" x1="-100.12369537353516" x2="100.12369537353516" y1="95.83332824707031" y2="95.83332824707031"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="39.453125" height="18.333332061767578" transform="translate( -19.7265625, -99.33332824707031)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Asset</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -92.62369537353516, -64.99999618530273)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject><foreignObject width="91.25" height="18.333332061767578" transform="translate( -92.62369537353516, -42.666664123535156)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-ETag: string</span></div></foreignObject><foreignObject width="148.828125" height="18.333332061767578" transform="translate( -92.62369537353516, -20.333332061767578)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-ContentType: string</span></div></foreignObject><foreignObject width="118.203125" height="18.333332061767578" transform="translate( -92.62369537353516, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Location: string</span></div></foreignObject><foreignObject width="86.51041412353516" height="18.333332061767578" transform="translate( -92.62369537353516, 24.333332061767578)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-MD5: string</span></div></foreignObject><foreignObject width="99.1796875" height="18.333332061767578" transform="translate( -92.62369537353516, 46.666664123535156)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Orphan: bool</span></div></foreignObject><foreignObject width="185.2473907470703" height="18.333332061767578" transform="translate( -92.62369537353516, 68.99999618530273)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-ProcessedMetadata: bool</span></div></foreignObject></g></g><g class="node default" id="classid-Photo-177" transform="translate(828.0813722610474, 452.6666450500488)"><rect class="outer title-state" x="-103.14453887939453" y="-162.66665840148926" width="206.28907775878906" height="325.3333168029785"/><line class="divider" x1="-103.14453887939453" x2="103.14453887939453" y1="-132.33332633972168" y2="-132.33332633972168"/><line class="divider" x1="-103.14453887939453" x2="103.14453887939453" y1="151.66665840148926" y2="151.66665840148926"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="43.33333206176758" height="18.333332061767578" transform="translate( -21.66666603088379, -155.16665840148926)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Photo</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -95.64453887939453, -120.83332633972168)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject><foreignObject width="119.77864074707031" height="18.333332061767578" transform="translate( -95.64453887939453, -98.4999942779541)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-BlurHash: string</span></div></foreignObject><foreignObject width="93.50260162353516" height="18.333332061767578" transform="translate( -95.64453887939453, -76.16666221618652)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Make: string</span></div></foreignObject><foreignObject width="99.24478912353516" height="18.333332061767578" transform="translate( -95.64453887939453, -53.833330154418945)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Model: string</span></div></foreignObject><foreignObject width="116.69270324707031" height="18.333332061767578" transform="translate( -95.64453887939453, -31.499998092651367)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-LensInfo: string</span></div></foreignObject><foreignObject width="125.546875" height="18.333332061767578" transform="translate( -95.64453887939453, -9.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-LensMake: string</span></div></foreignObject><foreignObject width="167.7473907470703" height="18.333332061767578" transform="translate( -95.64453887939453, 13.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-DateTaken: time.Time</span></div></foreignObject><foreignObject width="138.07290649414062" height="18.333332061767578" transform="translate( -95.64453887939453, 35.49999809265137)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Longitude: float64</span></div></foreignObject><foreignObject width="127.4609375" height="18.333332061767578" transform="translate( -95.64453887939453, 57.833330154418945)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Latitude: float64</span></div></foreignObject><foreignObject width="94.01041412353516" height="18.333332061767578" transform="translate( -95.64453887939453, 80.16666221618652)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Altitude: int</span></div></foreignObject><foreignObject width="191.28907775878906" height="18.333332061767578" transform="translate( -95.64453887939453, 102.4999942779541)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-ProcessedPreviews: string</span></div></foreignObject><foreignObject width="176.51040649414062" height="18.333332061767578" transform="translate( -95.64453887939453, 124.83332633972168)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-ProcessedPeople: string</span></div></foreignObject></g></g><g class="node default" id="classid-Video-178" transform="translate(689.6178293228149, 1108.8332862854004)"><rect class="outer title-state" x="-54.01692581176758" y="-39.83333206176758" width="108.03385162353516" height="79.66666412353516"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="-9.5" y2="-9.5"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="28.833332061767578" y2="28.833332061767578"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="42.265625" height="18.333332061767578" transform="translate( -21.1328125, -32.33333206176758)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Video</span></div></foreignObject><foreignObject width="93.03385162353516" height="18.333332061767578" transform="translate( -46.51692581176758, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: shortID</span></div></foreignObject></g></g><g class="node default" id="classid-Month-179" transform="translate(477.88931369781494, 1256.8332824707031)"><rect class="outer title-state" x="-48.886722564697266" y="-39.83333206176758" width="97.77344512939453" height="79.66666412353516"/><line class="divider" x1="-48.886722564697266" x2="48.886722564697266" y1="-9.5" y2="-9.5"/><line class="divider" x1="-48.886722564697266" x2="48.886722564697266" y1="28.833332061767578" y2="28.833332061767578"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="46.25" height="18.333332061767578" transform="translate( -23.125, -32.33333206176758)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Month</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -41.386722564697266, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject></g></g><g class="node default" id="classid-Year-180" transform="translate(625.6627588272095, 1256.8332824707031)"><rect class="outer title-state" x="-48.886722564697266" y="-39.83333206176758" width="97.77344512939453" height="79.66666412353516"/><line class="divider" x1="-48.886722564697266" x2="48.886722564697266" y1="-9.5" y2="-9.5"/><line class="divider" x1="-48.886722564697266" x2="48.886722564697266" y1="28.833332061767578" y2="28.833332061767578"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="34.375" height="18.333332061767578" transform="translate( -17.1875, -32.33333206176758)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Year</span></div></foreignObject><foreignObject width="82.77344512939453" height="18.333332061767578" transform="translate( -41.386722564697266, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: string</span></div></foreignObject></g></g><g class="node default" id="classid-Place-181" transform="translate(803.8365802764893, 1256.8332824707031)"><rect class="outer title-state" x="-54.01692581176758" y="-39.83333206176758" width="108.03385162353516" height="79.66666412353516"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="-9.5" y2="-9.5"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="28.833332061767578" y2="28.833332061767578"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="40.026039123535156" height="18.333332061767578" transform="translate( -20.013019561767578, -32.33333206176758)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Place</span></div></foreignObject><foreignObject width="93.03385162353516" height="18.333332061767578" transform="translate( -46.51692581176758, 2)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: shortID</span></div></foreignObject></g></g><g class="node default" id="classid-Caption-182" transform="translate(918.0390520095825, 734.666633605957)"><rect class="outer title-state" x="-54.01692581176758" y="-50.99999809265137" width="108.03385162353516" height="101.99999618530273"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="-20.66666603088379" y2="-20.66666603088379"/><line class="divider" x1="-54.01692581176758" x2="54.01692581176758" y1="39.99999809265137" y2="39.99999809265137"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="57.252601623535156" height="18.333332061767578" transform="translate( -28.626300811767578, -43.49999809265137)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Caption</span></div></foreignObject><foreignObject width="93.03385162353516" height="18.333332061767578" transform="translate( -46.51692581176758, -9.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: shortID</span></div></foreignObject><foreignObject width="89.32291412353516" height="18.333332061767578" transform="translate( -46.51692581176758, 13.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Text: string</span></div></foreignObject></g></g><g class="node default" id="classid-Tag-183" transform="translate(314.53644943237305, 114.83332824707031)"><rect class="outer title-state" x="-56.2890625" y="-50.99999809265137" width="112.578125" height="101.99999618530273"/><line class="divider" x1="-56.2890625" x2="56.2890625" y1="-20.66666603088379" y2="-20.66666603088379"/><line class="divider" x1="-56.2890625" x2="56.2890625" y1="39.99999809265137" y2="39.99999809265137"/><g class="label"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel"></span></div></foreignObject><foreignObject class="classTitle" width="26.35416603088379" height="18.333332061767578" transform="translate( -13.177083015441895, -43.49999809265137)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">Tag</span></div></foreignObject><foreignObject width="93.03385162353516" height="18.333332061767578" transform="translate( -48.7890625, -9.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Key: shortID</span></div></foreignObject><foreignObject width="97.578125" height="18.333332061767578" transform="translate( -48.7890625, 13.166666030883789)"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><span class="nodeLabel">-Name: string</span></div></foreignObject></g></g></g></g></g></svg>
classDiagram
class Memory{
-Key: string
-Name: string
-Narrative: string
}
class User{
-Key: string
-Name: string
-Email: string
-PasswordHash: string
}
class Asset{
-Key: string
-ETag: string
-ContentType: string
-Location: string
-MD5: string
-Orphan: bool
-ProcessedMetadata: bool
}
class Photo {
-Key: string
-BlurHash: string
-Make: string
-Model: string
-LensInfo: string
-LensMake: string
-DateTaken: time.Time
-Longitude: float64
-Latitude: float64
-Altitude: int
-ProcessedPreviews: string
-ProcessedPeople: string
}
class Video{
-Key: shortID
}
class Month{
-Key: string
}
class Year{
-Key: string
}
class Place{
-Key: shortID
}
class Caption {
-Key: shortID
-Text: string
}
class Tag {
-Key: shortID
-Name: string
}
Asset "1" <|--|> "1" Photo : is a
Asset "1" <|--|> "1" Video : is a
Photo "∞" <|--|> "1" Month : taken on
Photo "∞" <|--|> "1" Year: taken on
Photo "∞" <|--|> "1" Place: taken at
Photo "1" <|--|> "1" Caption: has caption
Video "∞" <|--|> "1" Month : taken on
Video "∞" <|--|> "1" Year: taken on
Video "∞" <|--|> "1" Place: taken at
Video "1" <|--|> "1" Caption: has caption
Caption "1" <|--|> "1" User: author
Memory "∞" <|--|> "1" Photo: contians
Memory "∞" <|--|> "∞" Video: contians
Memory "∞" <|--|> "1" User: author
Memory "∞" <|--|> "∞" Memory: links to
Memory "1" <|--|> "1" Photo: cover
Tag "∞" <|--|> "∞" Photo: has tag
Tag "∞" <|--|> "∞" Video: has tag
User "∞" <|--|> "∞" Photo: favorite
User "∞" <|--|> "∞" Video: favorite
package data
type GraphNode struct {
Key string `json:"key"`
Name string `json:"name"`
Kind string `json:"kind"`
}
package data
import "fmt"
func getEdgeKey(nodeA GraphNode, verb string, nodeB GraphNode) string {
return fmt.Sprintf("%s:%s:%s", nodeA.Key, verb, nodeB.Key)
}
package data
import (
"fmt"
"strconv"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/eql"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/rs/zerolog/log"
"github.com/teris-io/shortid"
)
var _datesInitialized = false
type Month struct {
Key string `json:"key"`
Kind string `json:"kind"`
Name string `json:"name"`
}
type Year struct {
Key string `json:"key"`
Kind string `json:"kind"`
Name string `json:"name"`
}
func ScaffoldMonths() error {
var months = [12]string{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
}
gm := graphStore.GetGraphManager()
trans := graph.NewGraphTrans(gm)
for _, month := range months {
log.Debug().Msgf("Scaffolding month %s", month)
assetNode := data.NewGraphNode()
assetNode.SetAttr("key", month)
assetNode.SetAttr("name", month)
assetNode.SetAttr("kind", "month")
if err := trans.StoreNode(graphStore.GetGpart(), assetNode); err != nil {
return err
}
}
if err := trans.Commit(); err != nil {
return err
}
return nil
}
func TakenInYear(year int, edge GraphNode) (yearNode data.Node, yearEdge data.Edge, err error) {
yearNode = data.NewGraphNode()
yearNode.SetAttr("key", year)
yearNode.SetAttr("name", year)
yearNode.SetAttr("kind", "year")
yearEdge = data.NewGraphEdge()
yearEdge.SetAttr(data.NodeKey, shortid.MustGenerate())
yearEdge.SetAttr(data.NodeKind, "taken")
yearEdge.SetAttr(data.EdgeEnd1Key, year)
yearEdge.SetAttr(data.EdgeEnd1Kind, "year")
yearEdge.SetAttr(data.EdgeEnd1Role, "year")
yearEdge.SetAttr(data.EdgeEnd1Cascading, false)
yearEdge.SetAttr(data.EdgeEnd2Key, edge.Key)
yearEdge.SetAttr(data.EdgeEnd2Kind, edge.Kind)
yearEdge.SetAttr(data.EdgeEnd2Role, "type")
yearEdge.SetAttr(data.EdgeEnd2Cascading, false)
yearEdge.SetAttr(data.NodeName, edge.Key+" taken in "+strconv.Itoa(year))
return
}
func TakenInMonth(month int, edge GraphNode) (monthEdge data.Edge, err error) {
if !_datesInitialized {
ScaffoldMonths()
_datesInitialized = true
}
m := MonthIntToString(month)
monthEdge = data.NewGraphEdge()
// TODO Rework edge names to use "Node<-taken->Month", do the same for year
monthEdge.SetAttr(data.NodeKey, shortid.MustGenerate())
monthEdge.SetAttr(data.NodeKind, "taken")
monthEdge.SetAttr(data.EdgeEnd1Key, m)
monthEdge.SetAttr(data.EdgeEnd1Kind, "month")
monthEdge.SetAttr(data.EdgeEnd1Role, "month")
monthEdge.SetAttr(data.EdgeEnd1Cascading, false)
monthEdge.SetAttr(data.EdgeEnd2Key, edge.Key)
monthEdge.SetAttr(data.EdgeEnd2Kind, edge.Kind)
monthEdge.SetAttr(data.EdgeEnd2Role, "type")
monthEdge.SetAttr(data.EdgeEnd2Cascading, false)
monthEdge.SetAttr(data.NodeName, edge.Key+" taken in "+m)
return
}
func FindTakenInMonths(months []int) ([]GraphNode, error) {
if !_datesInitialized {
ScaffoldMonths()
_datesInitialized = true
}
query := "get month where "
for i, month := range months {
m := MonthIntToString(month)
query += fmt.Sprintf("key = '%s' ", m)
if i == len(months)-1 {
continue
}
query += "or "
}
query += "traverse month:taken:type: end show 2:n:kind, 2:n:key, 2:n:name"
gm := graphStore.GetGraphManager()
res, err := eql.RunQuery("monthQuery", graphStore.GetGpart(), query, gm)
if err != nil {
log.Error().Err(err).Msgf("Error running query: ", query)
return nil, err
}
results := make([]GraphNode, 0)
for _, r := range res.Rows() {
node := GraphNode{
Kind: r[0].(string),
Key: r[1].(string),
Name: r[2].(string),
}
results = append(results, node)
}
log.Debug().Msgf("Query found %d results", len(results))
return results, nil
}
func FindTakenInYears(years []int) ([]GraphNode, error) {
query := "get year where "
for i, year := range years {
query += fmt.Sprintf("key = '%d' ", year)
if i == len(years)-1 {
continue
}
query += "or "
}
query += "traverse year:taken:type: end show 2:n:kind, 2:n:key, 2:n:name"
gm := graphStore.GetGraphManager()
res, err := eql.RunQuery("yearQuery", graphStore.GetGpart(), query, gm)
if err != nil {
log.Error().Err(err).Msgf("Error running query: ", query)
return nil, err
}
results := make([]GraphNode, 0)
for _, r := range res.Rows() {
node := GraphNode{
Kind: r[0].(string),
Key: r[1].(string),
Name: r[2].(string),
}
results = append(results, node)
}
log.Debug().Msgf("Query found %d results", len(results))
return results, nil
}
func FindTakenInYearAndMonth(year int, month int) ([]GraphNode, error) {
if !_datesInitialized {
ScaffoldMonths()
_datesInitialized = true
}
takenInMonth, err := FindTakenInMonths([]int{month})
if err != nil {
return nil, err
}
takenInYear, err := FindTakenInYears([]int{year})
if err != nil {
return nil, err
}
results := make([]GraphNode, 0)
for _, t := range takenInMonth {
for _, ty := range takenInYear {
if t.Key == ty.Key {
results = append(results, t)
}
}
}
return results, nil
}
func MonthIntToString(month int) string {
var months = [12]string{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
}
return months[month-1]
}
func MonthStringToInt(month string) int {
switch month {
case "January", "january", "Jan", "jan":
return 1
case "February", "february", "Feb", "feb":
return 2
case "March", "march", "Mar", "mar":
return 3
case "April", "april", "Apr", "apr":
return 4
case "May", "may":
return 5
case "June", "june", "Jun", "jun":
return 6
case "July", "july", "Jul", "jul":
return 7
case "August", "august", "Aug", "aug":
return 8
case "September", "september", "Sep", "sep":
return 9
case "October", "october", "Oct", "oct":
return 10
case "November", "november", "Nov", "nov":
return 11
case "December", "december", "Dec", "dec":
return 12
default:
return 0
}
}
package data
import (
"errors"
"strings"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/teris-io/shortid"
)
// A Caption is formated text associated with a particular item and author
type Caption struct {
GraphNode GraphNode
Text string `json:"name"`
CaptionedItem GraphNode `json:"captionedItem"`
}
// GetUpsertNode returns a graph node for a caption that is ready to upsert
func (c *Caption) GetUpsertNode(gm *graph.Manager, gpart string) (node data.Node, err error) {
existing, err := GetCaptionByKey(c.GraphNode.Key, gm, gpart)
if err != nil {
return nil, err
}
if c.CaptionedItem.Key == "" {
return nil, errors.New("CaptionedItem is required")
}
// If there is an existing Caption, then we need to update it
if existing.GraphNode.Key != "" {
c.GraphNode.Key = existing.GraphNode.Key
} else {
// If there is a caption already for this item, then we need to update it
existing, err := GetItemCaption(c.CaptionedItem, gm, gpart)
if err != nil {
return nil, err
}
if existing.GraphNode.Key != "" {
c.GraphNode = existing.GraphNode
} else {
// If there's no caption for this item then we need a new one.
c.GraphNode.Key = shortid.MustGenerate()
}
}
c.GraphNode.Kind = "caption"
c.Text = strings.TrimSpace(c.Text)
captionNode := data.NewGraphNode()
captionNode.SetAttr("key", c.GraphNode.Key)
captionNode.SetAttr("kind", c.GraphNode.Kind)
captionNode.SetAttr("text", c.Text)
return captionNode, nil
}
// CaptionsNode connects a caption and a node in the graph ("captioning" the node)
func (c *Caption) SetCaption(node GraphNode, gm *graph.Manager, gpart string) (err error) {
captionEdge := data.NewGraphEdge()
captionEdge.SetAttr(data.NodeKey, getEdgeKey(c.GraphNode, "captions", node))
captionEdge.SetAttr(data.NodeKind, "captionEdge")
captionEdge.SetAttr(data.NodeName, getEdgeKey(c.GraphNode, "captions", node))
captionEdge.SetAttr(data.EdgeEnd1Key, c.GraphNode.Key)
captionEdge.SetAttr(data.EdgeEnd1Kind, "caption")
captionEdge.SetAttr(data.EdgeEnd1Role, "captions")
captionEdge.SetAttr(data.EdgeEnd1Cascading, false)
captionEdge.SetAttr(data.EdgeEnd2Key, node.Key)
captionEdge.SetAttr(data.EdgeEnd2Kind, node.Kind)
captionEdge.SetAttr(data.EdgeEnd2Role, "captioned")
captionEdge.SetAttr(data.EdgeEnd2Cascading, false)
err = gm.StoreEdge(gpart, captionEdge)
if err != nil {
return err
}
return nil
}
// RemoveCaption removes a caption
func (c *Caption) RemoveCaption(node GraphNode, gm *graph.Manager, gpart string) (err error) {
_, err = gm.RemoveEdge(gpart, c.GraphNode.Key, "captionEdge")
if err != nil {
return err
}
_, err = gm.RemoveNode(gpart, c.GraphNode.Key, "caption")
if err != nil {
return err
}
return nil
}
// Upsert adds or updates a Caption in the graph
func (c *Caption) Upsert(gm *graph.Manager, gpart string) error {
node, err := c.GetUpsertNode(gm, gpart)
if err != nil {
return err
}
if err := gm.StoreNode(gpart, node); err != nil {
return nil
}
// Set Edge - we set this here to enforce a one-to-one relationship...
captionEdge := data.NewGraphEdge()
// For Captions the Edge key and node key are the same, because a caption can only have one edge
captionEdge.SetAttr(data.NodeKey, c.GraphNode.Key)
captionEdge.SetAttr(data.NodeKind, "captionEdge")
captionEdge.SetAttr(data.NodeName, getEdgeKey(c.GraphNode, "captions", c.CaptionedItem))
captionEdge.SetAttr(data.EdgeEnd1Key, c.GraphNode.Key)
captionEdge.SetAttr(data.EdgeEnd1Kind, "caption")
captionEdge.SetAttr(data.EdgeEnd1Role, "captions")
captionEdge.SetAttr(data.EdgeEnd1Cascading, false)
captionEdge.SetAttr(data.EdgeEnd2Key, c.CaptionedItem.Key)
captionEdge.SetAttr(data.EdgeEnd2Kind, c.CaptionedItem.Kind)
captionEdge.SetAttr(data.EdgeEnd2Role, "captioned")
// NOTE: we cascade delete the captions if the item is nuked, this prevents orphaned captions
captionEdge.SetAttr(data.EdgeEnd2Cascading, true)
err = gm.StoreEdge(gpart, captionEdge)
if err != nil {
return err
}
return nil
}
// GetCaptionByKey returns a Caption by its key
func GetCaptionByKey(key string, gm *graph.Manager, gpart string) (Caption, error) {
n, err := gm.FetchNode(gpart, key, "caption")
e, err := gm.FetchEdge(gpart, key, "captionEdge")
if err != nil {
return Caption{}, err
}
if n != nil && n.Key() != "" {
return nodeToCaption(n, e), nil
}
return Caption{}, nil
}
// Get Item Caption returns the caption for to the given item.
func GetItemCaption(item GraphNode, gm *graph.Manager, gpart string) (c Caption, err error) {
traversal := "::captions:caption"
nodes, _, err := gm.TraverseMulti(gpart, item.Key, item.Kind, traversal, true)
if err != nil {
return c, err
}
if len(nodes) > 0 {
c, err = GetCaptionByKey(nodes[0].Key(), gm, gpart)
if err != nil {
return c, err
}
}
return c, nil
}
// SearchCaptions searches for captions using word search.
func SearchCaptions(terms []string, gm *graph.Manager, gpart string) ([]Caption, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "caption")
if idxerr == nil {
if idx == nil {
return []Caption{}, nil
}
termResults := []map[string][]uint64{}
for _, term := range terms {
tResult, err := idx.LookupWord("text", term)
if err != nil {
return []Caption{}, err
}
termResults = append(termResults, tResult)
}
keys := GetSortedResultKeys(resultsMap)
if err == nil {
var captions []Caption
for _, key := range keys {
caption, err := GetCaptionByKey(key, gm, gpart)
if err != nil {
return captions, err
}
captions = append(captions, caption)
}
return captions, nil
}
}
return []Caption{}, idxerr
}
func sortByTermDistance([]map[string][]uint64 termResults) []map[string][]uint64 {
for
return nil
}
// Converts a caption graph node to a Caption object
func nodeToCaption(n data.Node, e data.Edge) Caption {
data := n.Data()
c := Caption{
GraphNode: GraphNode{
Key: n.Key(),
Kind: "caption",
},
Text: data["text"].(string),
CaptionedItem: GraphNode{
Key: e.End2Key(),
Kind: e.End2Kind(),
},
}
return c
}
package data
import (
"errors"
"devt.de/krotik/eliasdb/graph"
"devt.de/krotik/eliasdb/graph/data"
"github.com/teris-io/shortid"
)
type Asset struct {
GraphNode GraphNode
ETag string `json:"eTag,omitempty"`
ContentType string `json:"content_type,omitempty"`
Location string `json:"location,omitempty"`
Orphan bool `json:"orphan,omitempty"`
ProcessedMetadata string `json:"processed_metadata,omitempty"`
Md5 string `json:"md5,omitempty"`
}
type AssetProcessing string
const (
Metadata AssetProcessing = "Metadata"
Md5 AssetProcessing = "Md5"
)
func (a Asset) GetUpsertNode(gm *graph.Manager, gpart string) (node data.Node, err error) {
existing, err := GetAssetByETag(a.ETag, gm, gpart)
if err != nil {
return nil, err
}
// If there is an existing asset, then we need to scaffold a new one
if existing.GraphNode.Key != "" {
a.GraphNode.Key = existing.GraphNode.Key
} else {
a.GraphNode.Key = shortid.MustGenerate()
a.GraphNode.Kind = "asset"
a.GraphNode.Name = a.GraphNode.Key
a.Orphan = false
a.ProcessedMetadata = "false"
}
assetNode := data.NewGraphNode()
assetNode.SetAttr("key", a.GraphNode.Key)
assetNode.SetAttr("eTag", a.ETag)
assetNode.SetAttr("name", a.ETag)
assetNode.SetAttr("kind", "asset")
if a.ContentType != "" {
assetNode.SetAttr("content_type", a.ContentType)
}
if a.Location != "" {
assetNode.SetAttr("location", a.Location)
}
assetNode.SetAttr("orphan", a.Orphan)
assetNode.SetAttr("processed_metadata", a.ProcessedMetadata)
if a.Md5 != "" {
assetNode.SetAttr("md5", a.Md5)
} else {
assetNode.SetAttr("md5", "false")
}
return assetNode, nil
}
func (a Asset) Upsert(gm *graph.Manager, gpart string) error {
trans := graph.NewGraphTrans(gm)
node, err := a.GetUpsertNode(gm, gpart)
if err != nil {
return err
}
if err := trans.StoreNode(gpart, node); err != nil {
return err
}
return trans.Commit()
}
func (a *Asset) GetTypeEdge(gm *graph.Manager, gpart string, node GraphNode) (edge data.Edge, err error) {
edge = data.NewGraphEdge()
edge.SetAttr(data.NodeKey, shortid.MustGenerate())
edge.SetAttr(data.NodeKind, "assetIs")
edge.SetAttr(data.EdgeEnd1Key, a.GraphNode.Key)
edge.SetAttr(data.EdgeEnd1Kind, "asset")
edge.SetAttr(data.EdgeEnd1Role, "asset")
edge.SetAttr(data.EdgeEnd1Cascading, true)
edge.SetAttr(data.EdgeEnd2Key, node.Key)
edge.SetAttr(data.EdgeEnd2Kind, node.Kind)
edge.SetAttr(data.EdgeEnd2Role, "type")
edge.SetAttr(data.EdgeEnd2Cascading, false)
edge.SetAttr(data.NodeName, a.GraphNode.Key+" is a "+node.Key)
return edge, nil
}
func GetAllAssets(gm *graph.Manager, gpart string) ([]Asset, error) {
it, err := gm.NodeKeyIterator(gpart, "asset")
if err != nil {
return nil, err
}
var assets []Asset
if it == nil {
return assets, nil
}
for it.HasNext() {
key := it.Next()
if it.LastError != nil {
break
}
n, err := gm.FetchNode(gpart, key, "asset")
if err != nil {
return assets, err
}
assets = append(assets, nodeToAsset(n))
}
return assets, nil
}
func GetAllOrphans(gm *graph.Manager, gpart string) ([]Asset, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "asset")
if idxerr == nil {
keys, err := idx.LookupValue("orphan", "true")
if err != nil {
return nil, err
}
var orphans []Asset
for _, key := range keys {
orphan, err := GetAssetByKey(key, gm, gpart)
orphans = append(orphans, orphan)
if err != nil {
return nil, err
}
}
return orphans, nil
}
return nil, nil
}
func GetUnprocessedAssets(p AssetProcessing, gm *graph.Manager, gpart string) ([]Asset, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "asset")
if idxerr == nil && idx != nil {
var keys []string
var err error
switch p {
case Metadata:
keys, err = idx.LookupValue("processed_metadata", "false")
case Md5:
keys, err = idx.LookupValue("md5", "false")
default:
return nil, errors.New("Invalid processing type")
}
if err != nil {
return nil, err
}
var assetsToProcess []Asset
for _, key := range keys {
asset, err := GetAssetByKey(key, gm, gpart)
if !asset.Orphan {
assetsToProcess = append(assetsToProcess, asset)
}
if err != nil {
return nil, err
}
}
return assetsToProcess, nil
}
return nil, nil
}
func GetAssetByKey(key string, gm *graph.Manager, gpart string) (Asset, error) {
n, err := gm.FetchNode(gpart, key, "asset")
if err != nil {
return Asset{}, err
}
if n != nil && n.Key() != "" {
return nodeToAsset(n), nil
}
return Asset{}, nil
}
func GetAssetByETag(eTag string, gm *graph.Manager, gpart string) (Asset, error) {
idx, idxerr := gm.NodeIndexQuery(gpart, "asset")
if idxerr == nil {
if idx == nil {
return Asset{}, nil
}
keys, err := idx.LookupPhrase("eTag", eTag)
if err == nil {
if len(keys) > 0 {
n, err := gm.FetchNode(gpart, keys[0], "asset")
if err != nil {
return Asset{}, err
}
return nodeToAsset(n), nil
} else {
return Asset{}, nil
}
}
}
return Asset{}, idxerr
}
func nodeToAsset(n data.Node) Asset {
data := n.Data()
a := Asset{
GraphNode: GraphNode{
Key: n.Key(),
Kind: "asset",
Name: data["name"].(string),
},
ETag: data["eTag"].(string),
}
if data["contentType"] != nil {
a.ContentType = data["contentType"].(string)
}
if data["location"] != nil {
a.Location = data["location"].(string)
}
if data["orphan"] != nil {
a.Orphan = data["orphan"].(bool)
}
if data["processed_metadata"] != nil {
a.ProcessedMetadata = data["processed_metadata"].(string)
}
if data["md5"] != nil {
a.Md5 = data["md5"].(string)
}
return a
}
# Map of Data Models
![Map](map.svg)
/*! tailwindcss v2.2.19 | MIT License | https://tailwindcss.com */
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */
/*
Document
========
*/
/**
Use a better box model (opinionated).
*/
*,
::before,
::after {
box-sizing: border-box;
}
/**
Use a more readable tab size (opinionated).
*/
html {
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
}
/**
1. Correct the line height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
}
/*
Sections
========
*/
/**
Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
*/
body {
font-family:
system-ui,
-apple-system, /* Firefox supports this but not yet `system-ui` */
'Segoe UI',
Roboto,
Helvetica,
Arial,
sans-serif,
'Apple Color Emoji',
'Segoe UI Emoji';
}
/*
Grouping content
================
*/
/**
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
}
/*
Text-level semantics
====================
*/
/**
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/**
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
2. Correct the odd 'em' font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family:
ui-monospace,
SFMono-Regular,
Consolas,
'Liberation Mono',
Menlo,
monospace;
/* 1 */
font-size: 1em;
/* 2 */
}
/**
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
Tabular data
============
*/
/**
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
}
/*
Forms
=====
*/
/**
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-size: 100%;
/* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
}
/**
Remove the inheritance of text transform in Edge and Firefox.
1. Remove the inheritance of text transform in Firefox.
*/
button,
select {
/* 1 */
text-transform: none;
}
/**
Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
}
/**
Remove the inner border and padding in Firefox.
*/
/**
Restore the focus styles unset by the previous rule.
*/
/**
Remove the additional ':invalid' styles in Firefox.
See: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737
*/
/**
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
*/
legend {
padding: 0;
}
/**
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/**
Correct the cursor style of increment and decrement buttons in Safari.
*/
/**
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/**
Remove the inner padding in Chrome and Safari on macOS.
*/
/**
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to 'inherit' in Safari.
*/
/*
Interactive
===========
*/
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/**
* Manually forked from SUIT CSS Base: https://github.com/suitcss/base
* A thin layer on top of normalize.css that provides a starting point more
* suitable for web applications.
*/
/**
* Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
button {
background-color: transparent;
background-image: none;
}
fieldset {
margin: 0;
padding: 0;
}
ol,
ul {
list-style: none;
margin: 0;
padding: 0;
}
/**
* Tailwind custom reset styles
*/
/**
* 1. Use the user's configured `sans` font-family (with Tailwind's default
* sans-serif font stack as a fallback) as a sane default.
* 2. Use Tailwind's default "normal" line-height so the user isn't forced
* to override it to ensure consistency even when using the default theme.
*/
html {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 1 */
line-height: 1.5;
/* 2 */
}
/**
* Inherit font-family and line-height from `html` so users can set them as
* a class directly on the `html` element.
*/
body {
font-family: inherit;
line-height: inherit;
}
/**
* 1. Prevent padding and border from affecting element width.
*
* We used to set this in the html element and inherit from
* the parent element for everything else. This caused issues
* in shadow-dom-enhanced elements like <details> where the content
* is wrapped by a div with box-sizing set to `content-box`.
*
* https://github.com/mozdevs/cssremedy/issues/4
*
*
* 2. Allow adding a border to an element by just adding a border-width.
*
* By default, the way the browser specifies that an element should have no
* border is by setting it's border-style to `none` in the user-agent
* stylesheet.
*
* In order to easily add borders to elements by just setting the `border-width`
* property, we change the default border-style for all elements to `solid`, and
* use border-width to hide them instead. This way our `border` utilities only
* need to set the `border-width` property instead of the entire `border`
* shorthand, making our border utilities much more straightforward to compose.
*
* https://github.com/tailwindcss/tailwindcss/pull/116
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: currentColor;
/* 2 */
}
/*
* Ensure horizontal rules are visible by default
*/
hr {
border-top-width: 1px;
}
/**
* Undo the `border-style: none` reset that Normalize applies to images so that
* our `border-{width}` utilities have the expected effect.
*
* The Normalize reset is unnecessary for us since we default the border-width
* to 0 on all elements.
*
* https://github.com/tailwindcss/tailwindcss/issues/362
*/
img {
border-style: solid;
}
textarea {
resize: vertical;
}
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
color: #a1a1aa;
}
input:-ms-input-placeholder, textarea:-ms-input-placeholder {
opacity: 1;
color: #a1a1aa;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: #a1a1aa;
}
button {
cursor: pointer;
}
/**
* Override legacy focus reset from Normalize with modern Firefox focus styles.
*
* This is actually an improvement over the new defaults in Firefox in our testing,
* as it triggers the better focus styles even for links, which still use a dotted
* outline in Firefox by default.
*/
table {
border-collapse: collapse;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/**
* Reset links to optimize for opt-in styling instead of
* opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/**
* Reset form element properties that are easy to forget to
* style explicitly so you don't inadvertently introduce
* styles that deviate from your design system. These styles
* supplement a partial reset that is already applied by
* normalize.css.
*/
button,
input,
optgroup,
select,
textarea {
padding: 0;
line-height: inherit;
color: inherit;
}
/**
* Use the configured 'mono' font family for elements that
* are expected to be rendered with a monospace font, falling
* back to the system monospace stack if there is no configured
* 'mono' font family.
*/
pre,
code,
kbd,
samp {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/**
* 1. Make replaced elements `display: block` by default as that's
* the behavior you want almost all of the time. Inspired by
* CSS Remedy, with `svg` added as well.
*
* https://github.com/mozdevs/cssremedy/issues/14
*
* 2. Add `vertical-align: middle` to align replaced elements more
* sensibly by default when overriding `display` by adding a
* utility like `inline`.
*
* This can trigger a poorly considered linting error in some
* tools but is included by design.
*
* https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/**
* Constrain images and videos to the parent width and preserve
* their intrinsic aspect ratio.
*
* https://github.com/mozdevs/cssremedy/issues/14
*/
img,
video {
max-width: 100%;
height: auto;
}
/**
* Ensure the default browser behavior of the `hidden` attribute.
*/
[hidden] {
display: none;
}
*, ::before, ::after {
border-color: currentColor;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.static {
position: static;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.top-0 {
top: 0px;
}
.top-1 {
top: 0.25rem;
}
.top-2 {
top: 0.5rem;
}
.top-3 {
top: 0.75rem;
}
.top-2\.5 {
top: 0.625rem;
}
.top-3\.5 {
top: 0.875rem;
}
.-top-1 {
top: -0.25rem;
}
.right-0 {
right: 0px;
}
.right-3 {
right: 0.75rem;
}
.right-4 {
right: 1rem;
}
.left-0 {
left: 0px;
}
.left-1 {
left: 0.25rem;
}
.-left-1 {
left: -0.25rem;
}
.z-10 {
z-index: 10;
}
.z-50 {
z-index: 50;
}
.float-right {
float: right;
}
.float-left {
float: left;
}
.m-2 {
margin: 0.5rem;
}
.m-4 {
margin: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mt-2 {
margin-top: 0.5rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-3 {
margin-bottom: 0.75rem;
}
.ml-0 {
margin-left: 0px;
}
.ml-2 {
margin-left: 0.5rem;
}
.ml-3 {
margin-left: 0.75rem;
}
.block {
display: block;
}
.inline-block {
display: inline-block;
}
.flex {
display: flex;
}
.table {
display: table;
}
.hidden {
display: none;
}
.h-4 {
height: 1rem;
}
.h-6 {
height: 1.5rem;
}
.h-12 {
height: 3rem;
}
.h-16 {
height: 4rem;
}
.h-20 {
height: 5rem;
}
.h-auto {
height: auto;
}
.h-1\/6 {
height: 16.666667%;
}
.h-full {
height: 100%;
}
.h-screen {
height: 100vh;
}
.min-h-screen {
min-height: 100vh;
}
.w-4 {
width: 1rem;
}
.w-6 {
width: 1.5rem;
}
.w-9 {
width: 2.25rem;
}
.w-10 {
width: 2.5rem;
}
.w-80 {
width: 20rem;
}
.w-1\/2 {
width: 50%;
}
.w-1\/3 {
width: 33.333333%;
}
.w-full {
width: 100%;
}
.max-w-sm {
max-width: 24rem;
}
.max-w-lg {
max-width: 32rem;
}
.max-w-full {
max-width: 100%;
}
.flex-1 {
flex: 1 1 0%;
}
.flex-grow {
flex-grow: 1;
}
@-webkit-keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@-webkit-keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes ping {
75%, 100% {
transform: scale(2);
opacity: 0;
}
}
@-webkit-keyframes pulse {
50% {
opacity: .5;
}
}
@keyframes pulse {
50% {
opacity: .5;
}
}
@-webkit-keyframes bounce {
0%, 100% {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
animation-timing-function: cubic-bezier(0.8,0,1,1);
}
50% {
transform: none;
-webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
animation-timing-function: cubic-bezier(0,0,0.2,1);
}
}
@keyframes bounce {
0%, 100% {
transform: translateY(-25%);
-webkit-animation-timing-function: cubic-bezier(0.8,0,1,1);
animation-timing-function: cubic-bezier(0.8,0,1,1);
}
50% {
transform: none;
-webkit-animation-timing-function: cubic-bezier(0,0,0.2,1);
animation-timing-function: cubic-bezier(0,0,0.2,1);
}
}
.cursor-pointer {
cursor: pointer;
}
.resize-x {
resize: horizontal;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
--tw-space-y-reverse: 0;
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
}
.overflow-hidden {
overflow: hidden;
}
.overflow-scroll {
overflow: scroll;
}
.overflow-y-auto {
overflow-y: auto;
}
.overflow-y-scroll {
overflow-y: scroll;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-full {
border-radius: 9999px;
}
.rounded-t-lg {
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.border {
border-width: 1px;
}
.border-none {
border-style: none;
}
.border-opal-dark {
--tw-border-opacity: 1;
border-color: rgba(102, 163, 150, var(--tw-border-opacity));
}
.bg-gold {
--tw-bg-opacity: 1;
background-color: rgba(246, 174, 45, var(--tw-bg-opacity));
}
.bg-gray-paper {
--tw-bg-opacity: 1;
background-color: rgba(242, 244, 248, var(--tw-bg-opacity));
}
.bg-gray-lightest {
--tw-bg-opacity: 1;
background-color: rgba(229, 233, 240, var(--tw-bg-opacity));
}
.bg-gray {
--tw-bg-opacity: 1;
background-color: rgba(52, 66, 91, var(--tw-bg-opacity));
}
.bg-opal-light {
--tw-bg-opacity: 1;
background-color: rgba(204, 224, 220, var(--tw-bg-opacity));
}
.bg-opal-dark {
--tw-bg-opacity: 1;
background-color: rgba(102, 163, 150, var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
}
.hover\:bg-gold-light:hover {
--tw-bg-opacity: 1;
background-color: rgba(250, 211, 137, var(--tw-bg-opacity));
}
.hover\:bg-opal:hover {
--tw-bg-opacity: 1;
background-color: rgba(150, 192, 183, var(--tw-bg-opacity));
}
.focus\:bg-opal:focus {
--tw-bg-opacity: 1;
background-color: rgba(150, 192, 183, var(--tw-bg-opacity));
}
.object-right {
-o-object-position: right;
object-position: right;
}
.p-1 {
padding: 0.25rem;
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.p-5 {
padding: 1.25rem;
}
.p-12 {
padding: 3rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.px-6 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.py-3 {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.pb-4 {
padding-bottom: 1rem;
}
.pb-6 {
padding-bottom: 1.5rem;
}
.pl-10 {
padding-left: 2.5rem;
}
.text-left {
text-align: left;
}
.align-middle {
vertical-align: middle;
}
.font-mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.font-normal {
font-weight: 400;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
.tracking-tight {
letter-spacing: -0.025em;
}
.text-gold-dark {
--tw-text-opacity: 1;
color: rgba(215, 143, 9, var(--tw-text-opacity));
}
.text-gray-light {
--tw-text-opacity: 1;
color: rgba(125, 146, 181, var(--tw-text-opacity));
}
.text-gray {
--tw-text-opacity: 1;
color: rgba(52, 66, 91, var(--tw-text-opacity));
}
.text-gray-dark {
--tw-text-opacity: 1;
color: rgba(46, 58, 77, var(--tw-text-opacity));
}
.text-gray-black {
--tw-text-opacity: 1;
color: rgba(22, 28, 39, var(--tw-text-opacity));
}
.text-white {
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
}
.placeholder-gray-lighter::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgba(177, 189, 210, var(--tw-placeholder-opacity));
}
.placeholder-gray-lighter:-ms-input-placeholder {
--tw-placeholder-opacity: 1;
color: rgba(177, 189, 210, var(--tw-placeholder-opacity));
}
.placeholder-gray-lighter::placeholder {
--tw-placeholder-opacity: 1;
color: rgba(177, 189, 210, var(--tw-placeholder-opacity));
}
.placeholder-gray-light::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgba(125, 146, 181, var(--tw-placeholder-opacity));
}
.placeholder-gray-light:-ms-input-placeholder {
--tw-placeholder-opacity: 1;
color: rgba(125, 146, 181, var(--tw-placeholder-opacity));
}
.placeholder-gray-light::placeholder {
--tw-placeholder-opacity: 1;
color: rgba(125, 146, 181, var(--tw-placeholder-opacity));
}
*, ::before, ::after {
--tw-shadow: 0 0 #0000;
}
.shadow {
--tw-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-lg {
--tw-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-2xl {
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-inner {
--tw-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.outline-none {
outline: 2px solid transparent;
outline-offset: 2px;
}
.focus\:outline-none:focus {
outline: 2px solid transparent;
outline-offset: 2px;
}
*, ::before, ::after {
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgba(59, 130, 246, 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
}
.focus\:ring:focus {
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
}
.drop-shadow {
--tw-drop-shadow: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 1px rgba(0, 0, 0, 0.06));
}
.drop-shadow-xl {
--tw-drop-shadow: drop-shadow(0 20px 13px rgba(0, 0, 0, 0.03)) drop-shadow(0 8px 5px rgba(0, 0, 0, 0.08));
}
.transition {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
@media (min-width: 640px) {
.sm\:w-3\/4 {
width: 75%;
}
}
@media (min-width: 768px) {
}
@media (min-width: 1024px) {
}
@media (min-width: 1280px) {
}
@media (min-width: 1536px) {
}
<svg width="135" height="135" viewBox="0 0 135 135" xmlns="http://www.w3.org/2000/svg" fill="#111827">
<path d="M67.447 58c5.523 0 10-4.477 10-10s-4.477-10-10-10-10 4.477-10 10 4.477 10 10 10zm9.448 9.447c0 5.523 4.477 10 10 10 5.522 0 10-4.477 10-10s-4.478-10-10-10c-5.523 0-10 4.477-10 10zm-9.448 9.448c-5.523 0-10 4.477-10 10 0 5.522 4.477 10 10 10s10-4.478 10-10c0-5.523-4.477-10-10-10zM58 67.447c0-5.523-4.477-10-10-10s-10 4.477-10 10 4.477 10 10 10 10-4.477 10-10z">
<animateTransform
attributeName="transform"
type="rotate"
from="0 67 67"
to="-360 67 67"
dur="2.5s"
repeatCount="indefinite"/>
</path>
<path d="M28.19 40.31c6.627 0 12-5.374 12-12 0-6.628-5.373-12-12-12-6.628 0-12 5.372-12 12 0 6.626 5.372 12 12 12zm30.72-19.825c4.686 4.687 12.284 4.687 16.97 0 4.686-4.686 4.686-12.284 0-16.97-4.686-4.687-12.284-4.687-16.97 0-4.687 4.686-4.687 12.284 0 16.97zm35.74 7.705c0 6.627 5.37 12 12 12 6.626 0 12-5.373 12-12 0-6.628-5.374-12-12-12-6.63 0-12 5.372-12 12zm19.822 30.72c-4.686 4.686-4.686 12.284 0 16.97 4.687 4.686 12.285 4.686 16.97 0 4.687-4.686 4.687-12.284 0-16.97-4.685-4.687-12.283-4.687-16.97 0zm-7.704 35.74c-6.627 0-12 5.37-12 12 0 6.626 5.373 12 12 12s12-5.374 12-12c0-6.63-5.373-12-12-12zm-30.72 19.822c-4.686-4.686-12.284-4.686-16.97 0-4.686 4.687-4.686 12.285 0 16.97 4.686 4.687 12.284 4.687 16.97 0 4.687-4.685 4.687-12.283 0-16.97zm-35.74-7.704c0-6.627-5.372-12-12-12-6.626 0-12 5.373-12 12s5.374 12 12 12c6.628 0 12-5.373 12-12zm-19.823-30.72c4.687-4.686 4.687-12.284 0-16.97-4.686-4.686-12.284-4.686-16.97 0-4.687 4.686-4.687 12.284 0 16.97 4.686 4.687 12.284 4.687 16.97 0z">
<animateTransform
attributeName="transform"
type="rotate"
from="0 67 67"
to="360 67 67"
dur="8s"
repeatCount="indefinite"/>
</path>
</svg>
/* Toggle */
input:checked ~ .dot {
transform: translateX(100%);
background-color: #48bb78;
}
/* Radio Panel */
.radio-panel {
display: flex;
justify-content: center;
align-items: flex-start;
position: relative;
}
.radio-panel-button {
width: 20px;
height: 20px;
border: 2px solid #7D92B5;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
position: relative;
margin: 10px;
z-index: 9;
background: #B1BDD2;
}
.radio-panel-button input {
display: none;
}
.radio-panel-button .radio-panel-checkmark {
width: 100%;
height: 100%;
background-color: #F6AE2D;
border-radius: 50%;
display: inline-block;
opacity: 0;
transition: opacity 0.3s ease;
border: solid 3px #F6AE2D;
}
.radio-panel-button input:checked + .radio-panel-checkmark {
opacity: 1;
display: inline-block;
}
.radio-panel-label {
transform: rotate(35deg);
position: absolute;
top: 15px;
left: 18px;
width: 30px;
}
.radio-panel-track{
display: flex;
border: solid 4px #B1BDD2;
width: 80%;
position: absolute;
left: 0px;
top: 39%;
z-index: 1;
border-radius: 3px;
margin-left : 10px;
}
/* Tags */
.tag{
}
.tag-link {
}
.tag-remove {
border-radius: 50%;
width: 15px;
height: 15px;
right: -6px;
top: -7px;
position: absolute;
background: #ED5C5A;
text-align: center;
font-size: .6rem;
border: 0px;
padding-bottom: 6px;
}
/* Search Spinner */
.search-spinner-image{
position: absolute;
margin: 2px;
padding: 8px;
background: white;
height: 40px;
top: 1px;
width: 40px;
}
package cmd
import (
"fmt"
"webster/MemoryLane/data"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// userSetpassCmd represents the userSetpass command
var userSetpassCmd = &cobra.Command{
Use: "user-setpass",
Short: "Set or reset an account password",
Long: `Set or reset an account password.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Setting User Password")
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
gm := graphStore.GetGraphManager()
username := cmd.Flags().Lookup("username").Value.String()
password := cmd.Flags().Lookup("password").Value.String()
if username == "" || password == "" {
fmt.Println("Please provide all the required parameters")
return
}
user, err := data.GetUserByUsername(username, gm, gpart)
if err != nil {
fmt.Printf("Error setting password for user: %s", err.Error())
log.Fatal().Msg(err.Error())
return
}
err = user.SetPassword(password, gm, gpart)
if err != nil {
fmt.Printf("Error setting password for user: %s", err.Error())
log.Fatal().Msg(err.Error())
return
}
fmt.Printf("User %s password set.\n", user.GraphNode.Key)
fmt.Println("Checking Password")
match, _ := user.CheckPassword(password, gm, gpart)
if match {
fmt.Printf("Password match for user %s.\n", user.GraphNode.Key)
} else {
fmt.Printf("Password mismatch for user %s.\n", user.GraphNode.Key)
}
fmt.Println("Double Checking Password")
match, _ = user.CheckPassword("nope", gm, gpart)
if match {
fmt.Printf("Password mismatch for user %s.\n", user.GraphNode.Key)
} else {
fmt.Printf("Password check completed for user %s.\n", user.GraphNode.Key)
}
fmt.Printf("User %s's password set successfully\n", username)
},
}
func init() {
rootCmd.AddCommand(userSetpassCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// userSetpassCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// userSetpassCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
userSetpassCmd.Flags().StringP("username", "u", "", "User name")
userSetpassCmd.Flags().StringP("password", "p", "", "New password")
}
package cmd
import (
"fmt"
"webster/MemoryLane/data"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// userAddCmd represents the userAdd command
var userAddCmd = &cobra.Command{
Use: "user-add",
Short: "Add a new user",
Long: `Add a new user`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Adding User")
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
gm := graphStore.GetGraphManager()
username := cmd.Flags().Lookup("username").Value.String()
email := cmd.Flags().Lookup("email").Value.String()
password := cmd.Flags().Lookup("password").Value.String()
if username == "" || password == "" || email == "" {
fmt.Println("Please provide all the required parameters")
return
}
user := data.User{
UserName: username,
Email: email,
}
fmt.Println("Creating User.")
err = user.Upsert(graphStore.GetGraphManager(), graphStore.GetGpart())
if err != nil {
fmt.Printf("Error adding user: %s", err.Error())
log.Fatal().Err(err).Msg("Error while adding user.")
return
}
fmt.Printf("User %s added successfully.\n", user.GraphNode.Key)
fmt.Println("Setting User Password.")
err = user.SetPassword(password, gm, gpart)
if err != nil {
fmt.Printf("Error setting password: %s", err.Error())
log.Fatal().Err(err).Msg("Error while setting password.")
return
}
fmt.Printf("User %s password set.\n", user.GraphNode.Key)
fmt.Println("Checking Password")
match, _ := user.CheckPassword(password, gm, gpart)
if match {
fmt.Printf("Password match for user %s.\n", user.GraphNode.Key)
} else {
fmt.Printf("Password mismatch for user %s.\n", user.GraphNode.Key)
}
fmt.Println("Double Checking Password")
match, _ = user.CheckPassword("nope", gm, gpart)
if match {
fmt.Printf("Password mismatch for user %s.\n", user.GraphNode.Key)
} else {
fmt.Printf("Password check completed for user %s.\n", user.GraphNode.Key)
}
fmt.Printf("User %s added successfully\n", username)
},
}
func init() {
rootCmd.AddCommand(userAddCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// userAddCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
userAddCmd.Flags().StringP("username", "u", "", "User name")
userAddCmd.Flags().StringP("password", "p", "", "User password")
userAddCmd.Flags().StringP("email", "e", "", "User email")
}
package cmd
import (
"fmt"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"webster/MemoryLane/web"
"github.com/davidbyttow/govips/v2/vips"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// serverCmd represents the server command
var serverCmd = &cobra.Command{
Use: "server",
Short: "Run the server.",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Running server...")
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
vips.LoggingSettings(global.VipsLogger, vips.LogLevelDebug)
vips.Startup(nil)
defer vips.Shutdown()
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
fmt.Println(err)
return
}
web.Server()
},
}
func init() {
rootCmd.AddCommand(serverCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// serverCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "MemoryLane",
Short: "Organizing photos and more into memories.",
Long: `It's not about organizing photos we will never look at -
it's about link digital assets to personal memories
taking a people-first approach.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
cobra.OnInitialize(initConfig)
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.MemoryLane.yaml)")
rootCmd.PersistentFlags().BoolP("forceRestart", "r", false, "Force rescan, instead of picking up where we left off.")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
viper.SetConfigFile(cfgFile)
} else {
// Find home directory.
home, err := os.UserHomeDir()
cobra.CheckErr(err)
// Search config in home directory with name ".MemoryLane" (without extension).
viper.AddConfigPath(home)
viper.SetConfigType("yaml")
viper.SetConfigName(".MemoryLane")
}
viper.AutomaticEnv() // read in environment variables that match
// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
} else {
fmt.Fprintln(os.Stderr, "Error reading config file.", err)
}
}
package cmd
import (
"fmt"
"webster/MemoryLane/graphStore"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// backupCmd represents the backup command
var backupCmd = &cobra.Command{
Use: "backup",
Short: "Backs up the DB.",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("backup called")
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
fmt.Println(err)
return
}
gm := graphStore.GetGraphManager()
err = graphStore.Backup(gpart, gm)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
}
fmt.Print("Backup done.\n")
},
}
func init() {
rootCmd.AddCommand(backupCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// backupCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// backupCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
package cmd
import (
assets "webster/MemoryLane/assetStore"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
)
// scanCmd represents the scan command
var assetScanCmd = &cobra.Command{
Use: "asset-scan",
Short: "Scans the asset store into the graph.",
Long: `Scans the asset store and indexes assets into graph. Along the way it tags
them using the files structure.`,
Run: func(cmd *cobra.Command, args []string) {
var storager types.Storager
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
log.Debug().Caller().Msg("Scanning")
//Check to make sure we have variables set
if !viper.IsSet("AssetStorageType") {
log.Fatal().Msg("AssetStorageType not set")
return
}
switch viper.GetString("AssetStorageType") {
case "s3":
if !viper.IsSet("AssetS3AccessKeyID") {
log.Fatal().Msg("AssetS3AccessKeyID not set")
return
}
if !viper.IsSet("AssetS3SecretAccessKey") {
log.Fatal().Msg("AssetS3SecretAccessKey not set")
return
}
if !viper.IsSet("AssetS3Endpoint") {
log.Fatal().Msg("AssetS3Endpoint not set")
return
}
if !viper.IsSet("AssetS3Region") {
log.Fatal().Msg("AssetS3Region not set")
return
}
if !viper.IsSet("AssetS3Bucket") {
log.Fatal().Msg("AssetS3Bucket not set")
return
}
S3AccessKeyID := viper.GetString("AssetS3AccessKeyID")
S3SecretAccessKey := viper.GetString("AssetS3SecretAccessKey")
S3Endpoint := viper.GetString("AssetS3Endpoint")
S3Region := viper.GetString("AssetS3Region")
S3Bucket := viper.GetString("AssetS3Bucket")
s, err := assets.NewS3(S3AccessKeyID, S3SecretAccessKey, S3Endpoint, S3Region, S3Bucket)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
case "local":
if !viper.IsSet("AssetLocalPath") {
log.Fatal().Msg("AssetLocalPath not set")
return
}
path := viper.GetString("AssetLocalPath")
s, err := assets.NewLocal(path)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
}
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
forceRestart := cmd.Flag("forceRestart").Value.String() == "true"
assets.Scan(storager, gpart, forceRestart)
},
}
func init() {
rootCmd.AddCommand(assetScanCmd)
}
package cmd
import (
assets "webster/MemoryLane/assetStore"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"github.com/davidbyttow/govips/v2/vips"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
)
// assetProcessCmd represents the assetDownload command
var assetProcessCmd = &cobra.Command{
Use: "asset-process",
Short: "Process assets.",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
var storager types.Storager
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
vips.LoggingSettings(global.VipsLogger, vips.LogLevelDebug)
vips.Startup(nil)
defer vips.Shutdown()
//Check to make sure we have variables set
if !viper.IsSet("AssetStorageType") {
log.Fatal().Msg("AssetStorageType not set")
return
}
switch viper.GetString("AssetStorageType") {
case "s3":
if !viper.IsSet("AssetS3AccessKeyID") {
log.Fatal().Msg("AssetS3AccessKeyID not set")
return
}
if !viper.IsSet("AssetS3SecretAccessKey") {
log.Fatal().Msg("AssetS3SecretAccessKey not set")
return
}
if !viper.IsSet("AssetS3Endpoint") {
log.Fatal().Msg("AssetS3Endpoint not set")
return
}
if !viper.IsSet("AssetS3Region") {
log.Fatal().Msg("AssetS3Region not set")
return
}
if !viper.IsSet("AssetS3Bucket") {
log.Fatal().Msg("AssetS3Bucket not set")
return
}
S3AccessKeyID := viper.GetString("AssetS3AccessKeyID")
S3SecretAccessKey := viper.GetString("AssetS3SecretAccessKey")
S3Endpoint := viper.GetString("AssetS3Endpoint")
S3Region := viper.GetString("AssetS3Region")
S3Bucket := viper.GetString("AssetS3Bucket")
s, err := assets.NewS3(S3AccessKeyID, S3SecretAccessKey, S3Endpoint, S3Region, S3Bucket)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
case "local":
if !viper.IsSet("AssetLocalPath") {
log.Fatal().Msg("AssetLocalPath not set")
return
}
path := viper.GetString("AssetLocalPath")
s, err := assets.NewLocal(path)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
}
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
assets.Process(storager, gpart)
},
}
func init() {
rootCmd.AddCommand(assetProcessCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// assetProcessCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
}
package cmd
import (
"webster/MemoryLane/assetStore"
"webster/MemoryLane/global"
"webster/MemoryLane/graphStore"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
)
// orphanCheckCmd represents the orphanCheck command
var assetOrphanCheckCmd = &cobra.Command{
Use: "asset-orphan",
Short: "Check for 'orphans'",
Long: `Check for "orphans", or those files that ARE in our graph, but are NOT (or
are no longer) in the asset store.`,
Run: func(cmd *cobra.Command, args []string) {
var storager types.Storager
//Set the Global Log Level
closeLogger := global.SetupLogger()
defer closeLogger()
log.Debug().Caller().Msg("Checking for orphans")
//Check to make sure we have variables set
if !viper.IsSet("AssetStorageType") {
log.Fatal().Msg("AssetStorageType not set")
return
}
switch viper.GetString("AssetStorageType") {
case "s3":
if !viper.IsSet("AssetS3AccessKeyID") {
log.Fatal().Msg("AssetS3AccessKeyID not set")
return
}
if !viper.IsSet("AssetS3SecretAccessKey") {
log.Fatal().Msg("AssetS3SecretAccessKey not set")
return
}
if !viper.IsSet("AssetS3Endpoint") {
log.Fatal().Msg("AssetS3Endpoint not set")
return
}
if !viper.IsSet("AssetS3Region") {
log.Fatal().Msg("AssetS3Region not set")
return
}
if !viper.IsSet("AssetS3Bucket") {
log.Fatal().Msg("AssetS3Bucket not set")
return
}
S3AccessKeyID := viper.GetString("AssetS3AccessKeyID")
S3SecretAccessKey := viper.GetString("AssetS3SecretAccessKey")
S3Endpoint := viper.GetString("AssetS3Endpoint")
S3Region := viper.GetString("AssetS3Region")
S3Bucket := viper.GetString("AssetS3Bucket")
s, err := assetStore.NewS3(S3AccessKeyID, S3SecretAccessKey, S3Endpoint, S3Region, S3Bucket)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
case "local":
if !viper.IsSet("AssetLocalPath") {
log.Fatal().Msg("AssetLocalPath not set")
return
}
path := viper.GetString("AssetLocalPath")
s, err := assetStore.NewLocal(path)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
storager = s
}
gpart := viper.GetString("GraphPartition")
err := graphStore.InitGraphStore(viper.GetString("GraphName"), gpart)
if err != nil {
log.Fatal().Msg(err.Error())
return
}
forceRestart := cmd.Flag("forceRestart").Value.String() == "true"
assetStore.OrphanCheck(storager, gpart, forceRestart)
},
}
func init() {
rootCmd.AddCommand(assetOrphanCheckCmd)
}
/******************************************************************************
* S C A N A S S E T S *
******************************************************************************
*
* Scans the asset store and indexes assets into graph. Along the way it tags
* them using the files structure.
*
* This process makes a single pass over the entire file store, to build a list
* of "assets to process". Then it processes each of these into the graph,
* keeping track of it's progress by over-writing the "To Process List"
* periodically. The writing, and over writing of the "To Process List" is in-
* efficient, but builds in fault tolerance, in that this process can pick up
* where it left off if the process is interrupted (which is good, b/c it could
* be a hours long process).
*
*/
package assetStore
import (
"encoding/json"
"fmt"
"os"
"strings"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
// Add s3 support
"errors"
"devt.de/krotik/eliasdb/graph"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
)
// Scan the asset store and index assets into graph.
func Scan(store types.Storager, gpart string, forceRestart bool) {
log.Info().Msg("Scanning assets")
tmpPath := viper.GetString("TempLocation")
toProcessList := tmpPath + "/scanning/toprocess.json"
// If we are forcing a restart, delete the old list.
if forceRestart {
BuildToProcessList(store)
}
// If we don't have a list, build one.
log.Info().Msg("Reading in To Process List")
if _, err := os.Stat(toProcessList); err != nil {
log.Info().Msg("To Process list not found - building one.")
BuildToProcessList(store)
}
processAssets(ReadToProcessList(), gpart)
}
// Scan the asset store and build a list of assets to process.
func BuildToProcessList(store types.Storager) {
it, err := store.List("")
if err != nil {
log.Fatal().Err(err).Msg("Error listing files")
}
folders := 0
unsupported := 0
files := 0
var foundAssets []data.Asset
for {
if checkForShutdown() {
log.Info().Msg("Shutdown detected. Stopping scan.")
fmt.Println("\n\nShutdown detected. Stopping scan.")
break
}
// User can retrieve all the objects by `Next`. `types.IterateDone` will be returned while there is no item anymore.
o, err := it.Next()
if err != nil && !errors.Is(err, types.IterateDone) {
log.Debug().Msgf("Files: %d in %d", files, folders)
}
if err != nil {
log.Debug().Caller().Msg("list completed")
break
}
etag, _ := o.GetEtag()
path := o.GetPath()
contentType, _ := o.GetContentType()
etag = strings.Replace(etag, "\"", "", -1)
if etag == "d41d8cd98f00b204e9800998ecf8427e" {
folders++
continue
}
extension := strings.ToLower(path[strings.LastIndex(path, "."):])
if !SupportedFileType(contentType) && !SupportedExtension(extension) {
unsupported++
continue
}
md5 := "false"
if !strings.Contains(etag, "-") {
// If it contains a dash, it's a multipart upload, so etag will not equal standard md5, otherwise they will match.
md5 = etag
}
files++
newAsset := data.Asset{
ETag: etag,
Location: path,
ContentType: contentType,
Md5: md5,
}
foundAssets = append(foundAssets, newAsset)
if checkForShutdown() {
break
}
fmt.Printf("\r Found %d supported files in %d folders with %d unsupported files. Just logged etag: [%s] ", files, folders, unsupported, etag)
}
overwriteToProcessList(foundAssets)
log.Info().Msgf("Found To Process ==> Files: %d in %d folders.", files, folders)
}
// Process the assets in the list.
func processAssets(toProcess []data.Asset, gpart string) {
gm := graphStore.GetGraphManager()
const batchLimit = 25
// Get a list of assets from the graph
graphAssets, err := data.GetAllAssets(gm, gpart)
if err != nil {
log.Fatal().Err(err).Msg("Failed to get assets from graph")
}
found := 0
updated := 0
added := 0
counter := 0
trans := graph.NewConcurrentGraphTrans(gm)
// loop through the toProcess list and upsert any assets that are not in the graph or have a different location
p, _ := pbar.Pbar(len(toProcess))
p.SetDescription("Processing assets")
p.Initialize()
for _, asset := range toProcess {
if checkForShutdown() {
log.Info().Msg("Shutdown detected. Stopping scan processing.")
fmt.Println("\n\nShutdown detected. Stopping scan processing.")
break
}
inGraph := false
for _, graphAsset := range graphAssets {
if asset.ETag == graphAsset.ETag {
inGraph = true
found++
// asset is in the graph, check if it has a different location
if asset.Location != graphAsset.Location {
graphAsset.Location = asset.Location
node, err := graphAsset.GetUpsertNode(gm, gpart)
if err != nil {
log.Fatal().Err(err).Msg("Failed to upsert asset")
}
err = trans.StoreNode(gpart, node)
if err != nil {
log.Fatal().Err(err).Msg("Failed to upsert asset")
}
updated++
}
break
}
}
if inGraph {
p.Update()
continue
}
asset.Orphan = false
asset.ProcessedMetadata = "false"
node, err := asset.GetUpsertNode(gm, gpart)
if err != nil {
log.Fatal().Err(err).Msg("Failed to upsert asset")
}
err = trans.StoreNode(gpart, node)
if err != nil {
log.Fatal().Err(err).Msg("Failed to upsert asset")
}
added++
counter++
if counter >= batchLimit {
err = trans.Commit()
if err != nil {
log.Fatal().Err(err).Msg("Failed to commit transaction")
}
trans = graph.NewConcurrentGraphTrans(gm)
counter = 0
}
p.Update()
}
err = trans.Commit()
if err != nil {
log.Fatal().Err(err).Msg("Failed to commit transaction")
}
log.Info().Msgf("%d files scanned, %d already in graph, %d updated, %d added.", len(toProcess), found, updated, added)
fmt.Println("\n############################# S C A N #############################")
fmt.Printf("#\n#\t%d files scanned, %d already in graph, %d updated, %d added.\n#", len(toProcess), found, updated, added)
fmt.Println("\n####################################################################")
}
func overwriteToProcessList(assetsToProcess []data.Asset) {
tmpPath := viper.GetString("TempLocation")
toProcessList := tmpPath + "/scanning/toprocess.json"
scanningFolder := tmpPath + "/scanning"
if _, err := os.Stat(toProcessList); err == nil {
log.Debug().Msg("Nuking old process list.")
err := os.Remove(toProcessList) // remove a single file
if err != nil {
log.Fatal().Err(err).Msg("Failed to remove old process list.")
}
}
err := os.MkdirAll(scanningFolder, 0755)
if err != nil {
log.Fatal().Err(err).Msg("Failed to remove old process list.")
}
log.Debug().Msg("Writing out To Process List")
b, _ := json.MarshalIndent(assetsToProcess, "", " ")
// write out the temp file.
err = os.WriteFile(toProcessList, b, 0644)
if err != nil {
log.Fatal().Err(err).Msg("Failed to write out process list")
}
}
// Get the list of assets to process from the temp file
func ReadToProcessList() []data.Asset {
tmpPath := viper.GetString("TempLocation")
toProcessList := tmpPath + "/scanning/toprocess.json"
bytes, err := os.ReadFile(toProcessList)
if err != nil {
log.Fatal().Caller().Err(err).Msg("Failed to read in process list")
}
var toProcess []data.Asset
json.Unmarshal(bytes, &toProcess)
return toProcess
}
func SupportedFileType(contentType string) bool {
switch contentType {
case "image/jpeg", "image/jpg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp", "image/heic":
return true
default:
return false
}
}
func SupportedExtension(ext string) bool {
ext = strings.ToLower(ext)
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".tiff", ".bmp", ".webp", ".heic":
return true
default:
return false
}
}
package assetStore
import (
"context"
"fmt"
"webster/MemoryLane/global"
"github.com/rs/zerolog/log"
"go.beyondstorage.io/v5/types"
)
const _batchLimit = 25
const tryReprocess = false
type NumberLeft struct {
MD5s int
Metadata int
Previews int
Blurhashes int
MoreToProcess int
}
func Process(store types.Storager, gpart string) error {
fmt.Println("*********************")
fmt.Println("* Processing Assets *")
fmt.Println("*********************")
ctx := global.GetGlobalContext()
// This loop makes sure we process these in batches, avoiding downloading 75K files at once
batchLimit := _batchLimit
numberLeft, err := checkNumberLeft(store, gpart)
if err != nil {
return err
}
fmt.Printf("Beggining to process %d MD5s, %d Metadata, %d Previews, %d Blurhashes in batches of %d\n", numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes, batchLimit)
fmt.Println("\n####################################################################")
log.Info().Msgf("Begging to process %d MD5s, %d Metadata, %d Previews, %d Blurhashes in batches of %d", numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes, batchLimit)
batchesRun := 0
if numberLeft.MoreToProcess > 0 {
for numberLeft.MoreToProcess > 0 {
var err error
// Process MD5s
numberLeft.MD5s, err = ProcessMD5s(store, gpart, batchLimit, ctx)
if err != nil {
log.Error().Err(err).Caller().Msg("Error processing MD5s")
return err
}
if checkForShutdown() {
break
}
// Process Metadata
numberLeft.Metadata, err = ProcessMetadata(store, gpart, batchLimit)
if err != nil {
log.Warn().Err(err).Caller().Msg("Error processing metadata")
return err
}
if checkForShutdown() {
break
}
// Process Previews
numberLeft.Previews, err = ProcessPreviews(store, gpart, batchLimit)
if err != nil {
log.Warn().Err(err).Caller().Msg("Error processing previews")
return err
}
if checkForShutdown() {
break
}
// Blurhash
numberLeft.Blurhashes, err = ProcessBlurHashes(store, gpart, batchLimit)
if err != nil {
log.Error().Err(err).Caller().Msg("Error processing blur hashes")
return err
}
if checkForShutdown() {
break
}
numberLeft.CalculateMoreToProcess()
batchesRun++
CleanUpTemp()
fmt.Printf("Finished Batch %d. Left to process: %d MD5s, %d Metadata, %d Previews, %d Blurhashes", batchesRun, numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes)
log.Debug().Msgf("Finished Batch %d. Left to process: %d MD5s, %d Metadata, %d Previews, %d Blurhashes", batchesRun, numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes)
}
}
log.Info().Msgf("Processed %d of batches of %d run.", batchesRun, batchLimit)
fmt.Println("\n####################### P R O C E S S #############################")
fmt.Printf("#\n#\tProcessed %d of batches of %d run.\n#", batchesRun, batchLimit)
fmt.Println("\n####################################################################")
CleanUpTemp()
return nil
}
func checkForShutdown() bool {
select {
case <-global.GetGlobalContext().Done():
return true
default:
return false
}
}
func checkNumberLeft(store types.Storager, gpart string) (NumberLeft, error) {
ctx := context.Background()
var numberLeft NumberLeft
var err error
// Check for MD5s
numberLeft.MD5s, err = ProcessMD5s(store, gpart, 0, ctx)
if err != nil {
return numberLeft, err
}
// Check for Metadata
numberLeft.Metadata, err = ProcessMetadata(store, gpart, 0)
if err != nil {
return numberLeft, err
}
// Check for Previews
numberLeft.Previews, err = ProcessPreviews(store, gpart, 0)
if err != nil {
return numberLeft, err
}
// Check for Blurhashes
numberLeft.Blurhashes, err = ProcessBlurHashes(store, gpart, 0)
if err != nil {
return numberLeft, err
}
numberLeft.CalculateMoreToProcess()
return numberLeft, nil
}
func (numberLeft *NumberLeft) CalculateMoreToProcess() {
numberLeft.MoreToProcess = 0
left := [4]int{numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes}
for _, n := range left {
if n > numberLeft.MoreToProcess {
numberLeft.MoreToProcess = n
}
}
}
package assetStore
import (
"io/ioutil"
"os"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/graph"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
"github.com/davidbyttow/govips/v2/vips"
)
type PreviewSize string
const (
SmallSquare PreviewSize = "SmallSquare"
MediumSquare PreviewSize = "MediumSquare"
Medium PreviewSize = "Medium"
Full PreviewSize = "Full"
)
func ProcessPreviews(store types.Storager, gpart string, batchLimit int) (moreToProcess int, err error) {
gm := graphStore.GetGraphManager()
toProcess, err := data.GetUnprocessedPhotos(data.Previews, gm, gpart)
trans := graph.NewConcurrentGraphTrans(gm)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting unprocessed photos")
return 0, err
}
moreToProcess = len(toProcess) - batchLimit
plength := batchLimit
if len(toProcess) < batchLimit {
plength = len(toProcess)
moreToProcess = 0
}
if plength == 0 {
return moreToProcess, nil
}
p, _ := pbar.Pbar(plength)
p.SetDescription("Processing Previews")
p.Multi()
p.Initialize()
results := make(chan error, plength)
for i, photo := range toProcess {
go func(photo data.Photo, trans graph.Trans, i int) {
makePreviewsFromFile(&photo, store, gm, gpart, trans, results, i)
}(photo, trans, i)
if i+1 >= batchLimit {
break
}
}
for i := 0; i < plength; i++ {
err = <-results
if err != nil {
log.Error().Err(err).Caller().Msg("Processing previews")
return 0, err
}
p.Update()
}
err = trans.Commit()
if err != nil {
log.Error().Err(err).Caller().Msg("Error commiting transaction for previews")
return 0, err
}
close(results)
p.Update()
return moreToProcess, nil
}
func makePreviewsFromFile(photo *data.Photo, store types.Storager, gm *graph.Manager, gpart string, trans graph.Trans, results chan error, processNum int) {
//scaffold folder
previewsPath := viper.GetString("PreviewsLocation")
err := os.MkdirAll(previewsPath, 0755)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating previews directory")
results <- err
return
}
// Lookup Asset
asset, err := photo.GetAsset(gm, gpart)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting asset.")
results <- err
return
}
if asset.Orphan {
results <- nil
return
}
//download the asset
log.Info().Msgf("Downloading asset %s", asset.Location)
file, err := Download(store, &asset)
if err != nil {
log.Error().Err(err).Caller().Msg("Error downloading asset.")
results <- err
return
}
image, err := vips.NewImageFromFile(file)
if err != nil {
results <- handlePreviewError(photo, err, gm, gpart)
return
}
// Rotate the picture upright and reset EXIF orientation tag
err = image.AutoRotate()
if err != nil {
results <- handlePreviewError(photo, err, gm, gpart)
return
}
err = image.OptimizeICCProfile()
if err != nil {
results <- handlePreviewError(photo, err, gm, gpart)
return
}
errs := make(chan error, 4)
go makePreviews(photo, image, previewsPath, SmallSquare, gm, gpart, errs)
go makePreviews(photo, image, previewsPath, MediumSquare, gm, gpart, errs)
go makePreviews(photo, image, previewsPath, Medium, gm, gpart, errs)
go makePreviews(photo, image, previewsPath, Full, gm, gpart, errs)
for i := 0; i < 3; i++ {
err = <-errs
if err != nil {
log.Error().Err(err).Caller().Msg("Processing previews")
results <- err
return
}
}
photo.ProcessedPreviews = "true"
node, err := photo.GetUpsertNode(gm, gpart)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting node for photo")
results <- err
return
}
err = trans.StoreNode(gpart, node)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing node for photo")
results <- err
return
}
results <- nil
}
func makePreviews(photo *data.Photo, image *vips.ImageRef, previewPath string, size PreviewSize, gm *graph.Manager, gpart string, errs chan error) {
defer vips.ShutdownThread()
outputFolder := previewPath + "/square-small"
switch size {
case SmallSquare:
outputFolder = previewPath + "/square-small"
case MediumSquare:
outputFolder = previewPath + "/square-medium"
case Medium:
outputFolder = previewPath + "/medium"
case Full:
outputFolder = previewPath + "/full"
}
outputFile := outputFolder + "/" + photo.GraphNode.Key + ".webp"
err := os.MkdirAll(outputFolder, 0755)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating previews directory")
errs <- err
return
}
switch size {
case SmallSquare:
image.Thumbnail(100, 100, vips.InterestingAttention)
ep := vips.NewDefaultWEBPExportParams()
imagebytes, _, err := image.Export(ep)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
err = ioutil.WriteFile(outputFile, imagebytes, 0644)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
errs <- nil
return
case MediumSquare:
image.Thumbnail(300, 300, vips.InterestingAttention)
ep := vips.NewDefaultWEBPExportParams()
imagebytes, _, err := image.Export(ep)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
err = ioutil.WriteFile(outputFile, imagebytes, 0644)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
errs <- nil
return
case Medium:
//image.Resize(.5, vips.KernelLanczos3)
image.Thumbnail(500, 500, vips.InterestingNone)
ep := vips.NewDefaultWEBPExportParams()
imagebytes, _, err := image.Export(ep)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
err = ioutil.WriteFile(outputFile, imagebytes, 0644)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
errs <- nil
return
case Full:
image.Thumbnail(1000, 1000, vips.InterestingNone)
ep := vips.NewDefaultWEBPExportParams()
ep.Quality = 90
ep.Lossless = false
ep.Compression = 80
imagebytes, _, err := image.Export(ep)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
err = ioutil.WriteFile(outputFile, imagebytes, 0644)
if err != nil {
errs <- handlePreviewError(photo, err, gm, gpart)
return
}
errs <- nil
return
}
}
func handlePreviewError(photo *data.Photo, err error, gm *graph.Manager, gpart string) error {
log.Warn().Err(err).Msgf("Error creating previews for %s", photo.GraphNode.Key)
photo.ProcessedPreviews = "error"
return photo.Upsert(gm, gpart)
}
/******************************************************************************
* O R P H A N C H E C K
******************************************************************************
*
* Check for "orphans", or those files that ARE in our graph, but are NOT (or
* are no longer) in the asset store.
*
*/
package assetStore
import (
"fmt"
"os"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
)
// Check for "orphans", or those files that ARE in our graph, but are NOT (or
// are no longer) in the asset store.
func OrphanCheck(store types.Storager, gpart string, forceRestart bool) {
tmpPath := viper.GetString("TempLocation")
toProcessList := tmpPath + "/scanning/toprocess.json"
// If we are forcing a restart, delete the old list.
if forceRestart {
BuildToProcessList(store)
}
// If we don't have an asset list, build one.
log.Info().Msg("Reading in To Process List")
if _, err := os.Stat(toProcessList); err != nil {
log.Info().Msg("To Process list not found - building one.")
BuildToProcessList(store)
}
storeAssets := ReadToProcessList()
gm := graphStore.GetGraphManager()
// Scan the graph for all files
graphAssets, err := data.GetAllAssets(gm, gpart)
if err != nil {
log.Fatal().Err(err).Msg("Error getting all assets")
}
log.Debug().Msgf("Found %d assets in the graph", len(graphAssets))
// Check to see if they exist in the file system
orphansFound := 0
// for each graph asset, check to see if it exists in the store
p, _ := pbar.Pbar(len(graphAssets))
p.SetDescription("Checking for Orphans")
p.Initialize()
for _, graphAsset := range graphAssets {
found := false
var a data.Asset
//p.SetDescription(graphAsset["Asset.eTag"].(string))
if graphAsset.Orphan {
// If the asset is orphaned, we don't care about it.
continue
}
for _, storeAsset := range storeAssets {
a = storeAsset
//log.Debug().Msgf("Comparing %s to %s", graphAsset["Asset.eTag"], storeAsset.ETag)
if graphAsset.ETag == storeAsset.ETag {
//log.Debug().Msgf("Found.")
found = true
break
}
}
if !found {
// If they don't, then we have an orphan
log.Debug().Msgf("O R P H A N !: %s", graphAsset.ETag)
graphAsset.Orphan = true
err := graphAsset.Upsert(gm, gpart)
if err != nil {
log.Fatal().Caller().Err(err).Msgf("Failed upsert asset with etag %s", a.ETag)
}
orphansFound++
}
p.Update()
}
log.Info().Msgf("%d files checked, %d orphans.", len(graphAssets), orphansFound)
fmt.Println("\n########################### O R P H A N C H E C K ###########################")
fmt.Printf("#\n#\t%d files checked, %d orphans.\n#", len(graphAssets), orphansFound)
fmt.Println("\n##############################################################################")
}
package assetStore
import (
"bytes"
"strconv"
"strings"
"time"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/graph"
"github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"go.beyondstorage.io/v5/types"
)
func ProcessMetadata(store types.Storager, gpart string, batchLimit int) (moreToProcess int, err error) {
gm := graphStore.GetGraphManager()
toProcess, err := data.GetUnprocessedAssets(data.Metadata, gm, gpart)
trans := graph.NewConcurrentGraphTrans(gm)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting unprocessed assets")
return 0, err
}
moreToProcess = len(toProcess) - batchLimit
plength := batchLimit
if len(toProcess) < batchLimit {
plength = len(toProcess)
moreToProcess = 0
}
if plength == 0 {
return moreToProcess, nil
}
p, _ := pbar.Pbar(plength)
p.SetDescription("Processing Metadata")
p.Multi()
p.Initialize()
results := make(chan error, plength)
for i, asset := range toProcess {
go func(asset data.Asset, trans graph.Trans) {
processAssetMetadata(&asset, store, gm, gpart, trans, results)
}(asset, trans)
if i+1 >= batchLimit {
break
}
}
for i := 0; i < plength; i++ {
err = <-results
if err != nil {
log.Error().Err(err).Caller().Msg("Processing Metadata")
return 0, err
}
p.Update()
}
err = trans.Commit()
if err != nil {
log.Error().Err(err).Caller().Msg("Error commiting transaction for Metadata")
return 0, err
}
close(results)
p.Update()
return moreToProcess, nil
}
func processAssetMetadata(asset *data.Asset, store types.Storager, gm *graph.Manager, gpart string, trans graph.Trans, results chan error) {
var photo data.Photo
var err error
var month int
var year int
log.Info().Msgf("Processing asset %s", asset.GraphNode.Key)
if asset.Orphan {
results <- nil
return
}
if !tryReprocess && asset.ProcessedMetadata != "false" {
results <- nil
return
}
filePath, err := Download(store, asset)
if err != nil {
results <- handleMetadataError(asset, err, gm, gpart)
return
}
err = parseMetadata(filePath, &photo)
if err != nil {
results <- handleMetadataError(asset, err, gm, gpart)
return
}
if !photo.DateTaken.IsZero() {
month = int(photo.DateTaken.Month())
year = photo.DateTaken.Year()
} else {
//Check to see if the folder structure gives us a month...
pathStrings := strings.Split(asset.Location, "/")
for _, part := range pathStrings {
switch part {
case "January":
month = 1
continue
case "February":
month = 2
continue
case "March":
month = 3
continue
case "April":
month = 4
continue
case "May":
month = 5
continue
case "June":
month = 6
continue
case "July":
month = 7
continue
case "August":
month = 8
continue
case "September":
month = 9
continue
case "October":
month = 10
continue
case "November":
month = 11
continue
case "December":
month = 12
continue
}
i, _ := strconv.ParseInt(part, 10, 64)
if i > 1900 && i < 2100 { //i will be zero for non-numeric strings
year = int(i)
}
}
}
node, err := photo.GetUpsertNode(gm, gpart)
if err != nil {
results <- handleMetadataError(asset, err, gm, gpart)
return
}
err = trans.StoreNode(gpart, node)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing node on transaction.")
results <- err
return
}
if year != 0 {
yearNode, yearEdge, err := data.TakenInYear(year, photo.GraphNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating year node and edge.")
results <- err
return
}
err = trans.StoreNode(gpart, yearNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing year node on transaction.")
results <- err
return
}
err = trans.StoreEdge(gpart, yearEdge)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing year edge on transaction.")
results <- err
return
}
}
if month != 0 {
monthEdge, err := data.TakenInMonth(month, photo.GraphNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting month node.")
results <- err
return
}
err = trans.StoreEdge(gpart, monthEdge)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing month edge on transaction.")
results <- err
return
}
}
asset.ProcessedMetadata = "true"
assetNode, err := asset.GetUpsertNode(gm, gpart)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting upsert assest node")
results <- err
return
}
assetEdge, err := asset.GetTypeEdge(gm, gpart, photo.GraphNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting asset type edge")
results <- err
return
}
err = trans.StoreEdge(gpart, assetEdge)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing asset edge on transaction.")
results <- err
return
}
err = trans.StoreNode(gpart, assetNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing asset node on transaction.")
results <- err
return
}
results <- nil
}
func parseMetadata(filepath string, photo *data.Photo) error {
log.Info().Msgf("Parsing metadata for %s", filepath)
//Set default photo DateTime as now.
//Search and Extract ECIF
rawExif, err := exif.SearchFileAndExtractExif(filepath)
if err != nil {
log.Warn().Err(err).Msg("error extracting exif from file")
return err
}
// Set up a new Mapping with Standard Tags
im, err := exifcommon.NewIfdMappingWithStandard()
if err != nil {
log.Warn().Err(err).Msg("error mapping exif to standard tags")
return err
}
ti := exif.NewTagIndex()
//Build the tree of tags.
_, index, err := exif.Collect(im, ti, rawExif)
if err != nil {
log.Warn().Err(err).Msg("Error collecting tags")
return err
}
/*
* A note of the use of "string(bytes.Trim([]byte(valueRaw.(string)), "\x00"))"
*
* It turns out that the FUJI EX-1 camera likes to store EXIF data padded with extra 0 bits.
* This is a serious problem when you are expecting strings that can be compared, searched, etc.,
* and your strings contain extra invisible data.
*/
cb := func(ifd *exif.Ifd, ite *exif.IfdTagEntry) error {
//log.Debug().Caller().Msgf("Found tag: %s", ite.TagName())
switch ite.TagName() {
case "LensInfo":
valueRaw, _ := ite.Value()
log.Debug().Caller().Msgf("\t➪ LensInfo: %s", valueRaw.(string))
photo.LensInfo = string(bytes.Trim([]byte(valueRaw.(string)), "\x00"))
case "LensMake":
valueRaw, _ := ite.Value()
log.Debug().Caller().Msgf("\t➪ LensMake: %s", valueRaw.(string))
photo.LensMake = string(bytes.Trim([]byte(valueRaw.(string)), "\x00"))
case "Make":
valueRaw, _ := ite.Value()
log.Debug().Caller().Msgf("\t➪ Make: %s", valueRaw.(string))
photo.Make = string(bytes.Trim([]byte(valueRaw.(string)), "\x00"))
case "Model":
valueRaw, _ := ite.Value()
log.Debug().Caller().Msgf("\t➪ Model: %s", valueRaw.(string))
photo.Model = string(bytes.Trim([]byte(valueRaw.(string)), "\x00"))
case "DateTimeOriginal":
valueRaw, _ := ite.Value()
//Golang Template must use ➔ Mon Jan 2 15:04:05 MST 2006
//EXIF time format ➔ YYYY:MM:DD HH:MM:SS
photo.DateTaken, err = time.Parse("2006:01:02 15:04:05", string(bytes.Trim([]byte(valueRaw.(string)), "\x00")))
if err != nil {
log.Warn().Caller().Err(err).Msg("trouble parsing datetime")
}
log.Debug().Caller().Msgf("\t➪ DateTimeOriginal: %s", photo.DateTaken.Local())
}
return nil
}
err = index.RootIfd.EnumerateTagsRecursively(cb)
if err != nil {
log.Warn().Caller().Err(err).Msg("error extracting exif from file")
return err
}
ti = exif.NewTagIndex()
_, index, err = exif.Collect(im, ti, rawExif)
if err != nil {
log.Warn().Caller().Err(err).Msg("error extracting exif from file")
return err
}
ifd, err := index.RootIfd.ChildWithIfdPath(exifcommon.IfdGpsInfoStandardIfdIdentity)
if err != nil {
log.Warn().Msgf("Could not extract GPS from %s", filepath)
return nil
}
gi, err := ifd.GpsInfo()
photo.Longitude = gi.Longitude.Decimal()
photo.Latitude = gi.Latitude.Decimal()
photo.Altitude = gi.Altitude
return nil
}
func handleMetadataError(asset *data.Asset, err error, gm *graph.Manager, gpart string) error {
log.Warn().Err(err).Msgf("Error parsing metadata for asset %s", asset.Location)
asset.ProcessedMetadata = "error"
return asset.Upsert(gm, gpart)
}
package assetStore
import (
"context"
"crypto/md5"
"encoding/hex"
"io"
"os"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/graph"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"go.beyondstorage.io/v5/types"
)
func ProcessMD5s(store types.Storager, gpart string, batchLimit int, ctx context.Context) (moreToProcess int, err error) {
gm := graphStore.GetGraphManager()
toProcess, err := data.GetUnprocessedAssets(data.Md5, gm, gpart)
trans := graph.NewConcurrentGraphTrans(gm)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting unprocessed assets")
return 0, err
}
moreToProcess = len(toProcess) - batchLimit
plength := batchLimit
if len(toProcess) < batchLimit {
plength = len(toProcess)
moreToProcess = 0
}
if plength == 0 {
return moreToProcess, nil
}
p, _ := pbar.Pbar(plength)
p.SetDescription("Processing MD5s")
p.Multi()
p.Initialize()
results := make(chan error, plength)
for i, asset := range toProcess {
go func(asset data.Asset, trans graph.Trans) {
processAssetMd5(&asset, store, gm, gpart, trans, results)
}(asset, trans)
if i+1 >= batchLimit {
break
}
}
for i := 0; i < plength; i++ {
err = <-results
if err != nil {
log.Error().Err(err).Caller().Msg("Processing Md5")
return 0, err
}
p.Update()
}
err = trans.Commit()
if err != nil {
log.Error().Err(err).Caller().Msg("Error commiting transaction")
return 0, err
}
close(results)
return moreToProcess, nil
}
func processAssetMd5(asset *data.Asset, store types.Storager, gm *graph.Manager, gpart string, trans graph.Trans, results chan error) {
log.Info().Msgf("Processing asset %s", asset.GraphNode.Key)
if asset.Orphan {
results <- nil
return
}
filePath, err := Download(store, asset)
if err != nil {
results <- nil
return
}
asset.Md5, err = getMD5(filePath)
if err != nil {
results <- handleMd5Error(asset, err, gm, gpart)
return
}
upsertNode, err := asset.GetUpsertNode(gm, gpart)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting upsert node.")
results <- nil
return
}
err = trans.StoreNode(gpart, upsertNode)
if err != nil {
results <- nil
return
}
results <- nil
}
func getMD5(filePath string) (string, error) {
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
// deepcode ignore InsecureHash: This is not used for security purpose, but only for file integrity checks.
hash := md5.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func handleMd5Error(asset *data.Asset, err error, gm *graph.Manager, gpart string) error {
log.Warn().Err(err).Msgf("Error getting MD5 for asset %s", asset.Location)
asset.Md5 = "error"
return asset.Upsert(gm, gpart)
}
package assetStore
import (
"fmt"
"os"
"strings"
"webster/MemoryLane/data"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/pairs"
"go.beyondstorage.io/v5/types"
)
// Download a file from S3 to TempDir
func Download(store types.Storager, asset *data.Asset) (string, error) {
log.Info().Msg("Downloading asset")
tmpPath := viper.GetString("TempLocation")
pathParts := strings.Split(asset.Location, ".")
extention := pathParts[len(pathParts)-1]
downloadingFolder := tmpPath + "/processing"
toProcessFile := downloadingFolder + "/" + asset.GraphNode.Key + "." + extention
if _, err := os.Stat(toProcessFile); err == nil {
localHash, err := getMD5(toProcessFile)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting hash of local file.")
return "", err
}
log.Debug().Msgf("Local hash: %s ; Assest Hash %s", localHash, asset.Md5)
if asset.Md5 != "false" && localHash != asset.Md5 {
log.Debug().Msg("Hash mismatch, nuking old file.")
err := os.Remove(toProcessFile) // remove a single file
if err != nil {
log.Error().Err(err).Caller().Msg("Error deleting old file")
return "", err
}
} else {
return toProcessFile, nil
}
}
err := os.MkdirAll(downloadingFolder, 0755)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating temp directory")
return "", err
}
localFile, err := os.Create(toProcessFile)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating temp file")
return "", err
}
defer localFile.Close()
stat, err := store.Stat(asset.Location)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting asset stat")
return "", err
}
length := stat.MustGetContentLength()
cur := int64(0)
fn := func(bs []byte) {
cur += int64(len(bs))
percent := float64(cur) / float64(length) * 100
fmt.Printf("\r %s Downloading ... %d %%", asset.GraphNode.Key, int(percent))
}
// If IoCallback is specified, the storage will call it in every I/O operation.
// User could use this feature to implement progress bar.
_, err = store.Read(asset.Location, localFile, pairs.WithIoCallback(fn))
if err != nil {
log.Error().Err(err).Caller().Msg("Error downloading file")
return "", err
}
if strings.Contains(asset.ETag, "-") {
// If the S3 Etag contains a - it means it is a multi-part upload, and hash is impossible to calculate without knowning upload chuck size.
return toProcessFile, nil
} else {
localHash, err := getMD5(toProcessFile)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting hash of local file.")
return "", err
}
if localHash != asset.ETag {
log.Warn().Msgf("Hash mismatch on %s, nuking downloaded file to try again.", asset.GraphNode.Key)
err := os.Remove(toProcessFile) // remove a single file
if err != nil {
log.Error().Err(err).Caller().Msg("Error deleting old file")
return "", err
}
return Download(store, asset)
}
log.Debug().Msgf("Downloaded file hash matches asset hash for %s", asset.GraphNode.Key)
}
return toProcessFile, nil
}
/******************************************************************************
* S 3 C O N N E C T O R *
******************************************************************************
*
* Gets a NewS3 Connector using Beyond Storage.
*
*/
package assetStore
import (
"go.beyondstorage.io/services/s3/v3"
"go.beyondstorage.io/v5/pairs"
"go.beyondstorage.io/v5/types"
)
// Gets a NewS3 Storager.
func NewS3(S3AccessKeyID string, S3SecretAccessKey string, S3Endpoint string, S3Region string, S3Bucket string) (types.Storager, error) {
return s3.NewStorager(
// Credential could be fetched from service's console.
//
// Example Value: hmac:access_key_id:secret_access_key
pairs.WithCredential("hmac:"+S3AccessKeyID+":"+S3SecretAccessKey),
// endpoint: https://beyondstorage.io/docs/go-storage/pairs/endpoint
//
// endpoint is default to amazon s3's endpoint.
// If using s3 compatible services, please input their endpoint.
//
// Example Value: https:host:port
pairs.WithEndpoint(S3Endpoint),
// location: https://beyondstorage.io/docs/go-storage/pairs/location
//
// For s3, location is the bucket's zone.
// For s3 compatible services, location could be ignored or has other value,
// please refer to their documents.
//
// Example Value: ap-east-1
pairs.WithLocation(S3Region),
// name: https://beyondstorage.io/docs/go-storage/pairs/name
//
// name is the bucket name.
pairs.WithName(S3Bucket),
// features: https://beyondstorage.io/docs/go-storage/pairs/index#feature-pairs
//
// virtual_dir feature is designed for a service that doesn't have native dir support but wants to provide simulated operations.
// s3.WithEnableVirtualDir()
)
}
/******************************************************************************
* L O C A L C O N N E C T O R *
******************************************************************************
*
* Gets a Local FS Connector using Beyond Storage.
*
*/
package assetStore
import (
"github.com/rs/zerolog/log"
fs "go.beyondstorage.io/services/fs/v4"
"go.beyondstorage.io/v5/pairs"
"go.beyondstorage.io/v5/types"
)
// Gets a Local Storager.
func NewLocal(path string) (types.Storager, error) {
store, err := fs.NewStorager(pairs.WithWorkDir(path))
if err != nil {
log.Fatal().Err(err).Msg("Failed to create local storager")
}
return store, nil
}
package assetStore
import (
"os"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func CleanUpTemp() {
log.Info().Msg("Cleaning up temp files")
tmpPath := viper.GetString("TempLocation")
downloadingFolder := tmpPath + "/processing"
err := os.RemoveAll(downloadingFolder)
if err != nil {
log.Error().Err(err).Caller().Msg("Error cleaning up temp files")
}
}
package assetStore
import (
"image"
"os"
"webster/MemoryLane/data"
"webster/MemoryLane/graphStore"
"devt.de/krotik/eliasdb/graph"
"github.com/buckket/go-blurhash"
"github.com/kinsey40/pbar"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.beyondstorage.io/v5/types"
"golang.org/x/image/webp"
)
func ProcessBlurHashes(store types.Storager, gpart string, batchLimit int) (moreToProcess int, err error) {
gm := graphStore.GetGraphManager()
toProcess, err := data.GetUnprocessedPhotos(data.Blurhash, gm, gpart)
trans := graph.NewConcurrentGraphTrans(gm)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting unprocessed photos")
return 0, err
}
moreToProcess = len(toProcess) - batchLimit
plength := batchLimit
if len(toProcess) < batchLimit {
plength = len(toProcess)
moreToProcess = 0
}
if plength == 0 {
return moreToProcess, nil
}
p, _ := pbar.Pbar(plength)
p.SetDescription("Processing blurhashes")
p.Multi()
p.Initialize()
results := make(chan error, plength)
for i, photo := range toProcess {
go func(photo data.Photo, trans graph.Trans, i int) {
GetBlurHash(&photo, trans, gpart, gm, results, i)
}(photo, trans, i)
if i+1 >= batchLimit {
break
}
}
for i := 0; i < plength; i++ {
err = <-results
if err != nil {
log.Error().Err(err).Caller().Msg("Processing blurhash")
return 0, err
}
p.Update()
}
err = trans.Commit()
if err != nil {
log.Error().Err(err).Caller().Msg("Error commiting transaction for blurhash")
return 0, err
}
close(results)
return moreToProcess, nil
}
//GetBlurHash computes the blurhash of an image.
func GetBlurHash(photo *data.Photo, trans graph.Trans, gpart string, gm *graph.Manager, results chan error, processNum int) {
log.Debug().Caller().Msgf("Getting blurhash for: %s", photo.GraphNode.Key)
previewsPath := viper.GetString("PreviewsLocation")
imagePath := previewsPath + "/square-medium/" + photo.GraphNode.Key + ".webp"
// Read image from file that already exists
imageFile, err := os.Open(imagePath)
if err != nil {
log.Warn().Err(err).Caller().Msgf("Error reading preview file %s", imagePath)
// If we can't get to this file, we should recalculate the previews.
photo.ProcessedPreviews = "false"
} else {
defer imageFile.Close()
var img image.Image
img, err = webp.Decode(imageFile)
if err != nil {
log.Error().Err(err).Caller().Msg("Error decoding preview file.")
results <- err
}
bhash, err := blurhash.Encode(6, 6, img)
if err != nil {
log.Error().Err(err).Caller().Msg("Error creating blur hash.")
results <- err
}
photo.BlurHash = bhash
}
upsertNode, err := photo.GetUpsertNode(gm, gpart)
if err != nil {
log.Error().Err(err).Caller().Msg("Error getting upsert node.")
results <- err
}
err = trans.StoreNode(gpart, upsertNode)
if err != nil {
log.Error().Err(err).Caller().Msg("Error storing on transaction node.")
results <- err
}
results <- nil
}
# MemoryLane
> It's not about organizing photos we will never look at - it's about taking a people-first approach to link digital assets to personal memories.
# Asset Processing
## Hashes
Hash are stored as MD5's of the original file. We also store an eTag from S3. The MD5 is for clients to determine if an asset already exists. The S3 eTag is for syncing with S3.
## Meta Data
EXIF data is parse and stored on a [Photo node](/models/photo.go). Data stored is:
* Make
* Model
* Date Taken (trying for the best of the various date fields)
* Lens Make
* Lens Info
## Previews
For each asset we generate a preview asset, and store that previews location in the graph.
### Photo Previews
For photos we store webP file locations on the [Photo node](/models/photo.go).
* Full (1000 wide)
* Medium (500 wide)
* Square-medium (500x500)
* Square-small (100x100)
## Blur Hash
To support rapid loading and good UX, we produce and store a [blur hash](https://blurha.sh/) of the large preview file as a string on the [Photo node](/models/photo.go).
## Dates
Creation dates are stored in two ways. First on the relevant asset type node, e.g. ([Photo node](/models/photo.go)) a 'DateTaken' is stored. Dates are also stored in nodes for 'Month', 'Year' to allow for quick filtering (See [Date Nodes][/models/dates.go]).
## Geographic Places
Places are a map layer for collecting and sorting coordinate locations parsed from metadata and stored on relevant asset type node, e.g. ([Photo node](/models/photo.go)).
## People
<!--- //TODO update this when people are implemented -->
Machine learning can be used to detect faces and tag individuals. People tags can be stored as nodes connected to the relevant asset type node, e.g. ([Photo node](/models/photo.go)).
- - -
### Notes
To add a new command line command
```bash
$HOME/go/bin/cobra add {name of command}
```