PJDTNLZMD5A5CDNA73435G72AJLAATL3OIFWZFCYLDSICZ4HGL7AC package tagsimport ("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 tagsimport ("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.Contextgm := 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 tagsimport ("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.Contextgm := 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.GraphNodevar formTags []stringfor 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 itif 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 tagsimport ("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.Photofor _, 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 tagsimport ("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.Contextgm := 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 []stringfor 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 itif 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 sideViewimport ("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 sideViewimport ("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 webimport ("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 stackr.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 /loginr.Post("/", login.PostLogin) // POST /loginr.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 configfmt.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 securityimport ("context""fmt""net/http""webster/MemoryLane/data""webster/MemoryLane/graphStore")func SecurityCtx(next http.Handler) http.Handler {var ctx context.Contextreturn 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 securityimport "github.com/gorilla/securecookie"var _initializedCookies = falsevar _hashKey []bytevar _blockKey []bytefunc NewSecureCookie() *securecookie.SecureCookie {if !_initializedCookies {initializeCookies()}return securecookie.New(_hashKey, _blockKey)}// TODO - This should be called on a timed basis to reset the keysfunc 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 = truereturn}
package securityimport ("net/http""github.com/gorilla/csrf""github.com/spf13/viper")var _initialized = falsevar _csrfKey []bytefunc 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 searchimport ("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 searchimport ("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.Contextvar results = []data.GraphNode{}var err errort1 := 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 searchimport ("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.Photofor _, 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 searchimport ("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 photosimport ("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 photosimport ("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 loginimport ("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 := trueswitch runLevel {case "development":secure = falsecase "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 layoutsimport ("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 documentreturn c.HTML5(c.HTML5Props{Title: title,Language: "en",Head: head(),Body: []g.Node{// See https://play.tailwindcss.com/uOnWQzR9tlMain(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"),// SidebarDiv(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 "),// Headercomponents.Top(),),Div(Class("flex flex-1 bg-gray-paper overflow-y-auto paragraph px-4"),// Bodybody,),Div(Class("flex w-full bg-opal-light"),// Bottomcomponents.Bottom(),),),),g.Group(lists),),},})}func EmptyPage(title, path string, body g.Node) g.Node {// HTML5 boilerplate documentreturn 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 favoritesimport ("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 := falseuserFavs, 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 = truebreak}}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 favoritesimport ("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 exceptionsimport ("net/http")func ErrorPage(w http.ResponseWriter, message string) {w.WriteHeader(http.StatusInternalServerError)w.Write([]byte("<html><body><h1>" + message + "</h1></body></html>"))}
package componentsimport (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 componentsimport (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"),// ToggleDiv(Class("relative"),// InputInput(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),),// LineDiv(Class("w-10 h-4 bg-gray-400 rounded-full shadow-inner")),// DotDiv(Class("dot absolute w-6 h-6 bg-white rounded-full shadow -left-1 -top-1 transition")),),),// LabelDiv(Class("ml-3 text-gray-700 font-medium"),g.Text(rightLabel),),)return}
package componentsimport ("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 componentsimport (g "github.com/christophersw/gomponents-htmx". "github.com/christophersw/gomponents-htmx/html")type RadioPanelButton struct {Name stringValue stringLabel stringChecked 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 componentsimport ("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 componentsimport ("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 componentsimport ("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 := falsefavorites, 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 = truebreak}}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 componentsimport ("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 componentsimport (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 componentsimport (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 captionsimport ("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.Contextgm := 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.Contexterr := 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 captionsimport ("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-e7ea1fred: {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 athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License.*/package mainimport ("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 contextctx, 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-ignoreif (typeof define === 'function' && define.amd) {// AMD. Register as an anonymous module.//@ts-ignoredefine([], factory);} else {// Browser globalsroot.htmx = factory();}}(typeof self !== 'undefined' ? self : this, function () {return (function () {'use strict';// Public API//** @type {import("./htmx").HtmxApi} */// TODO: list all methods in public APIvar 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 prefixesfunction 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 JSUnresolvedVariablevar 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]*)/ivar 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-ignoreresponseNode = responseNode.firstChild;}if (responseNode == null) {// @ts-ignoreresponseNode = 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 attributeif (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) { // IEforEach(element.children, function(child) { cleanUpElement(child) });}}function swapOuterHTML(target, fragment, settleInfo) {if (target.tagName === "BODY") {return swapInnerHTML(target, fragment, settleInfo);} else {// @type {HTMLElement}var newEltvar 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 latersettleInfo.elts = [] // clear existing elementswhile(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 themfor (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 initScrollHandlervar 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 requestelt.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=13724form.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-ignorefragment = fragment.querySelector('[hx-history-elt],[data-hx-history-elt]') || fragment;var historyElement = getHistoryElement();var settleInfo = makeSettleInfo(historyElement);// @ts-ignoreswapInnerHTML(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 errorwindow.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 inputsif (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 setvar validate = matches(elt, 'form') && elt.noValidate !== true;// for a non-GET include the closest formif (verb !== 'get') {processInputValue(processed, formValues, errors, closest(elt, 'form'), validate);}// include the element itselfprocessInputValue(processed, values, errors, elt, validate);// if a button or submit was clicked last, include its valuevar internalData = getInternalData(elt);if (internalData.lastButtonClicked) {var name = getRawAttribute(internalData.lastButtonClicked,"name");if (name) {values[name] = internalData.lastButtonClicked.value;}}// include any explicit includesvar 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 itif (!matches(node, 'form')) {forEach(node.querySelectorAll(INPUT_SELECTOR), function (descendant) {processInputValue(processed, values, errors, descendant, validate);})}});}// form values take precedence, overriding the regular valuesvalues = 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 insteadxhr.setRequestHeader(header, encodeURIComponent(headerValue));xhr.setRequestHeader(header + "-URI-AutoEncoded", "true");}}}function getResponseURL(xhr) {// NB: IE11 does not support this stuffif (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 queueeltData.queuedRequests.push(function () {issueAjaxRequest(verb, path, elt, event, etc)});}return;} else {eltData.requestInFlight = true;}var endRequestLock = function(){eltData.requestInFlight = falseif (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 entryif (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 URLif (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 overwrittenpath = 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 headersif (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 DOMif (!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 propertyvar 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-targetingserverResponse = beforeSwapDetails.serverResponse; // allow updating contentisError = beforeSwapDetails.isError; // allow updating errorresponseInfo.failed = isError; // Make failed property available to response eventsresponseInfo.successful = !isError; // Make successful property available to response eventsif (beforeSwapDetails.shouldSwap) {if (xhr.status === 286) {cancelPolling(elt);}withExtensions(elt, function (extension) {serverResponse = extension.transformResponse(serverResponse, xhr, elt);});// Save current pageif (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-ignorestart: activeElt ? activeElt.selectionStart : null,// @ts-ignoreend: 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-ignoreif (selectionInfo.start && newActiveElt.setSelectionRange) {// @ts-ignorenewActiveElt.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 pageif (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-ignorereturn parseJSON(element.content);} else {return null;}}function mergeMetaConfig() {var metaConfig = getMetaConfig();if (metaConfig) {htmx.config = mergeObjects(htmx.config , metaConfig)}}// initialize the documentready(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 graphStoreimport ("fmt""webster/MemoryLane/global""devt.de/krotik/eliasdb/graph""devt.de/krotik/eliasdb/graph/graphstorage""github.com/rs/zerolog/log")var _initialized = falsevar _gs graphstorage.Storagevar _gpart stringvar _gm *graph.Managerfunc 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 storagegs, err := graphstorage.NewDiskGraphStorage(name, false)_gs = gs_gpart = gpartif err != nil {log.Logger.Fatal().Err(err).Msg("Could not create graph storage")return err}gm := graph.NewGraphManager(_gs)_gm = gm_initialized = trueglobal.RegisterShutdownAction(func() {log.Info().Msg("Closing Graph Storage.")fmt.Println("Closing Graph Storage .")_gs.Close()})}return nil}
package graphStoreimport ("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/MemoryLanego 1.17require (devt.de/krotik/eliasdb v1.2.0github.com/christophersw/gomponents-htmx v0.20.0github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82bgithub.com/go-chi/chi/v5 v5.0.7github.com/kinsey40/pbar v0.0.0-20190815161936-21f8229eaa8agithub.com/maragudk/gomponents v0.18.0github.com/rs/zerolog v1.26.0github.com/spf13/cobra v1.3.0github.com/spf13/viper v1.10.0github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125go.beyondstorage.io/services/s3/v3 v3.0.1go.beyondstorage.io/v5 v5.0.0golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8bgolang.org/x/image v0.0.0-20211028202545-6944b10bf410)require (devt.de/krotik/common v1.4.4 // indirectgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirectgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirectgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirectgithub.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirectgithub.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirectgithub.com/fsnotify/fsnotify v1.5.1 // indirectgithub.com/go-errors/errors v1.4.1 // indirectgithub.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirectgithub.com/hashicorp/hcl v1.0.0 // indirectgithub.com/inconshreveable/mousetrap v1.0.0 // indirectgithub.com/magiconair/properties v1.8.5 // indirectgithub.com/mitchellh/mapstructure v1.4.3 // indirectgithub.com/pkg/errors v0.9.1 // indirectgithub.com/qingstor/go-mime v0.1.0 // indirectgithub.com/spf13/afero v1.6.0 // indirectgithub.com/spf13/cast v1.4.1 // indirectgithub.com/spf13/jwalterweatherman v1.1.0 // indirectgithub.com/subosito/gotenv v1.2.0 // indirectgolang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirectgolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirectgolang.org/x/text v0.3.7 // indirectgopkg.in/ini.v1 v1.66.2 // indirectgopkg.in/yaml.v2 v2.4.0 // indirect)require (github.com/Xuanwo/gg v0.3.0 // indirectgithub.com/Xuanwo/go-bufferpool v0.2.0 // indirectgithub.com/Xuanwo/templateutils v0.2.0 // indirectgithub.com/aws/aws-sdk-go-v2 v1.11.2 // indirectgithub.com/aws/aws-sdk-go-v2/config v1.11.0 // indirectgithub.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirectgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirectgithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirectgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirectgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirectgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirectgithub.com/aws/aws-sdk-go-v2/service/s3 v1.21.0 // indirectgithub.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirectgithub.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirectgithub.com/aws/smithy-go v1.9.0 // indirectgithub.com/buckket/go-blurhash v1.1.0github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirectgithub.com/dave/dst v0.26.2 // indirectgithub.com/davidbyttow/govips/v2 v2.7.0github.com/gorilla/csrf v1.7.1github.com/gorilla/securecookie v1.1.1github.com/kevinburke/go-bindata v3.22.0+incompatible // indirectgithub.com/pelletier/go-toml v1.9.4 // indirectgithub.com/russross/blackfriday/v2 v2.1.0 // indirectgithub.com/sirupsen/logrus v1.8.1 // indirectgithub.com/spf13/pflag v1.0.5 // indirectgithub.com/urfave/cli/v2 v2.3.0 // indirectgo.beyondstorage.io/credential v1.0.0 // indirectgo.beyondstorage.io/endpoint v1.2.0 // indirectgo.beyondstorage.io/services/fs/v4 v4.0.0golang.org/x/mod v0.5.1 // indirectgolang.org/x/sys v0.0.0-20211214170744-3b038e5940ed // indirectgolang.org/x/tools v0.1.8 // indirectgolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect)
package globalimport ("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 globalimport ("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 LevellogLevel := 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 outputlog.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:" + messageswitch 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 globalimport ("context")var _ctx context.Contextfunc GetGlobalContext() context.Context {if _ctx == nil {_ctx := context.Background()return _ctx}return _ctx}func SetGlobalContext(ctx context.Context) {_ctx = ctx}
package dataimport "sort"type Result struct {Key stringScore int}type SearchResults []Resultfunc (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 SearchResultsfor 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 []stringfor _, r := range results {keys = append(keys, r.Key)}return keys}
package dataimport ("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 GraphNodeUserName string `json:"name"`Email string `json:"email"`Favorites []GraphNode `json:"favorites"`}var _usersInitialized = falsefunc ScaffoldUsers() error {_usersInitialized = truescaffoldUser := 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 oneif existing.GraphNode.Key != "" {u.GraphNode.Key = existing.GraphNode.Key} else {// Check to see if we can find by usernameuser, 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 emailuser, 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 10hashedPassword, 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 hasherr = 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 []Userif 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:" + itemKindnodes, _, err := gm.TraverseMulti(gpart, u.GraphNode.Key, u.GraphNode.Kind, traversal, false)if err != nil {return nil, err}var favoriteItems []GraphNodefor _, n := range nodes {var gNode GraphNodegNode.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 []Userfor _, n := range nodes {users = append(users, nodeToUser(n))}return users, nil}// RemoveFavorite removes a favorite from a userfunc (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 dataimport ("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 GraphNodeName string `json:"name"`}// GetUpsertNode returns a graph node for a tag that is ready to upsertfunc (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 oneif 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 graphfunc (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 nodefunc (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 graphfunc 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:" + itemKindnodes, _, err := gm.TraverseMulti(gpart, t.GraphNode.Key, t.GraphNode.Kind, traversal, false)if err != nil {return nil, err}var taggedItems []GraphNodefor _, n := range nodes {var gNode GraphNodegNode.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 []Tagfor _, n := range nodes {var tag Tagtag.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 keyfunc 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 namefunc 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 []Tagfor _, 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 objectfunc 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 dataimport ("errors""fmt""regexp""strconv""strings""webster/MemoryLane/graphStore")// Search takes a search string and returns a list of GraphNodesfunc Search(searchString string, wordsIn []string, user User, resultsIn [][]GraphNode) ([]GraphNode, error) {var words []stringif wordsIn != nil {words = wordsIn} else {words = strings.Split(searchString, " ")}deepestStart := 0deepestEnd := len(words) - 1deepestLevel := 0depthCurrent := 0for i, word := range words {if strings.HasPrefix(word, "(") {depthCurrent++if depthCurrent >= deepestLevel {deepestLevel = depthCurrentdeepestStart = ideepestEnd = i}} else if strings.HasSuffix(word, ")") {if depthCurrent == deepestLevel {deepestEnd = i}depthCurrent--}}var searchType stringvar searchTerms []stringvar command stringre := regexp.MustCompile(`:\d:`)results := []GraphNode{}var err error// Calculate the depth of the deepest parenthetical groupfor x := deepestStart; x <= deepestEnd; x++ {word := strings.Replace(words[x], "(", "", -1)word = strings.Replace(word, ")", "", -1)switch word {case "":continuecase "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 := wordsrout := 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 := falsefor _, result := range results {if result.Key == item.Key {skip = truebreak}}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 groupresults := []GraphNode{}gm := graphStore.GetGraphManager()gpart := graphStore.GetGpart()var err errorswitch 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 itemsfor _, 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 itemsfor _, caption := range captions {item := caption.CaptionedItemif 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 := falsefor _, b := range listB {if a.Key == b.Key {found = true}}if !found {results = append(results, a)}}return results}
package dataimport ("errors""time""devt.de/krotik/eliasdb/graph""devt.de/krotik/eliasdb/graph/data""github.com/teris-io/shortid")type Photo struct {GraphNode GraphNodeBlurHash 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 stringconst (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 oneif 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 []Photoif idxerr == nil && idx != nil {var keys []stringvar err errorswitch 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, nilcase 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>
classDiagramclass 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 aAsset "1" <|--|> "1" Video : is aPhoto "∞" <|--|> "1" Month : taken onPhoto "∞" <|--|> "1" Year: taken onPhoto "∞" <|--|> "1" Place: taken atPhoto "1" <|--|> "1" Caption: has captionVideo "∞" <|--|> "1" Month : taken onVideo "∞" <|--|> "1" Year: taken onVideo "∞" <|--|> "1" Place: taken atVideo "1" <|--|> "1" Caption: has captionCaption "1" <|--|> "1" User: authorMemory "∞" <|--|> "1" Photo: contiansMemory "∞" <|--|> "∞" Video: contiansMemory "∞" <|--|> "1" User: authorMemory "∞" <|--|> "∞" Memory: links toMemory "1" <|--|> "1" Photo: coverTag "∞" <|--|> "∞" Photo: has tagTag "∞" <|--|> "∞" Video: has tagUser "∞" <|--|> "∞" Photo: favoriteUser "∞" <|--|> "∞" Video: favorite
package datatype GraphNode struct {Key string `json:"key"`Name string `json:"name"`Kind string `json:"kind"`}
package dataimport "fmt"func getEdgeKey(nodeA GraphNode, verb string, nodeB GraphNode) string {return fmt.Sprintf("%s:%s:%s", nodeA.Key, verb, nodeB.Key)}
package dataimport ("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 = falsetype 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 yearmonthEdge.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 1case "February", "february", "Feb", "feb":return 2case "March", "march", "Mar", "mar":return 3case "April", "april", "Apr", "apr":return 4case "May", "may":return 5case "June", "june", "Jun", "jun":return 6case "July", "july", "Jul", "jul":return 7case "August", "august", "Aug", "aug":return 8case "September", "september", "Sep", "sep":return 9case "October", "october", "Oct", "oct":return 10case "November", "november", "Nov", "nov":return 11case "December", "december", "Dec", "dec":return 12default:return 0}}
package dataimport ("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 authortype Caption struct {GraphNode GraphNodeText string `json:"name"`CaptionedItem GraphNode `json:"captionedItem"`}// GetUpsertNode returns a graph node for a caption that is ready to upsertfunc (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 itif existing.GraphNode.Key != "" {c.GraphNode.Key = existing.GraphNode.Key} else {// If there is a caption already for this item, then we need to update itexisting, 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 captionfunc (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 graphfunc (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 edgecaptionEdge.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 captionscaptionEdge.SetAttr(data.EdgeEnd2Cascading, true)err = gm.StoreEdge(gpart, captionEdge)if err != nil {return err}return nil}// GetCaptionByKey returns a Caption by its keyfunc 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 []Captionfor _, 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 {forreturn nil}// Converts a caption graph node to a Caption objectfunc 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 dataimport ("errors""devt.de/krotik/eliasdb/graph""devt.de/krotik/eliasdb/graph/data""github.com/teris-io/shortid")type Asset struct {GraphNode GraphNodeETag 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 stringconst (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 oneif existing.GraphNode.Key != "" {a.GraphNode.Key = existing.GraphNode.Key} else {a.GraphNode.Key = shortid.MustGenerate()a.GraphNode.Kind = "asset"a.GraphNode.Name = a.GraphNode.Keya.Orphan = falsea.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 []Assetif 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 []Assetfor _, 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 []stringvar err errorswitch 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 []Assetfor _, 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
/*! 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"><animateTransformattributeName="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"><animateTransformattributeName="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 cmdimport ("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 commandvar 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 LevelcloseLogger := 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 cmdimport ("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 commandvar 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 LevelcloseLogger := 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 cmdimport ("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 commandvar 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 LevelcloseLogger := 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 cmdimport ("fmt""os""github.com/spf13/cobra""github.com/spf13/viper")var cfgFile string// rootCmd represents the base command when called without any subcommandsvar 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 memoriestaking 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 cmdimport ("fmt""webster/MemoryLane/graphStore""github.com/spf13/cobra""github.com/spf13/viper")// backupCmd represents the backup commandvar 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 cmdimport (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 commandvar 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 tagsthem using the files structure.`,Run: func(cmd *cobra.Command, args []string) {var storager types.Storager//Set the Global Log LevelcloseLogger := global.SetupLogger()defer closeLogger()log.Debug().Caller().Msg("Scanning")//Check to make sure we have variables setif !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 = scase "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 cmdimport (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 commandvar 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 LevelcloseLogger := global.SetupLogger()defer closeLogger()vips.LoggingSettings(global.VipsLogger, vips.LogLevelDebug)vips.Startup(nil)defer vips.Shutdown()//Check to make sure we have variables setif !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 = scase "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 cmdimport ("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 commandvar 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 (orare no longer) in the asset store.`,Run: func(cmd *cobra.Command, args []string) {var storager types.Storager//Set the Global Log LevelcloseLogger := global.SetupLogger()defer closeLogger()log.Debug().Caller().Msg("Checking for orphans")//Check to make sure we have variables setif !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 = scase "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 assetStoreimport ("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 := 0unsupported := 0files := 0var foundAssets []data.Assetfor {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 graphgraphAssets, err := data.GetAllAssets(gm, gpart)if err != nil {log.Fatal().Err(err).Msg("Failed to get assets from graph")}found := 0updated := 0added := 0counter := 0trans := graph.NewConcurrentGraphTrans(gm)// loop through the toProcess list and upsert any assets that are not in the graph or have a different locationp, _ := 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 := falsefor _, graphAsset := range graphAssets {if asset.ETag == graphAsset.ETag {inGraph = truefound++// asset is in the graph, check if it has a different locationif asset.Location != graphAsset.Location {graphAsset.Location = asset.Locationnode, 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 = falseasset.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 fileif 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 filefunc 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.Assetjson.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 truedefault:return false}}func SupportedExtension(ext string) bool {ext = strings.ToLower(ext)switch ext {case ".jpg", ".jpeg", ".png", ".gif", ".tiff", ".bmp", ".webp", ".heic":return truedefault:return false}}
package assetStoreimport ("context""fmt""webster/MemoryLane/global""github.com/rs/zerolog/log""go.beyondstorage.io/v5/types")const _batchLimit = 25const tryReprocess = falsetype NumberLeft struct {MD5s intMetadata intPreviews intBlurhashes intMoreToProcess 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 oncebatchLimit := _batchLimitnumberLeft, 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 := 0if numberLeft.MoreToProcess > 0 {for numberLeft.MoreToProcess > 0 {var err error// Process MD5snumberLeft.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 MetadatanumberLeft.Metadata, err = ProcessMetadata(store, gpart, batchLimit)if err != nil {log.Warn().Err(err).Caller().Msg("Error processing metadata")return err}if checkForShutdown() {break}// Process PreviewsnumberLeft.Previews, err = ProcessPreviews(store, gpart, batchLimit)if err != nil {log.Warn().Err(err).Caller().Msg("Error processing previews")return err}if checkForShutdown() {break}// BlurhashnumberLeft.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 truedefault:return false}}func checkNumberLeft(store types.Storager, gpart string) (NumberLeft, error) {ctx := context.Background()var numberLeft NumberLeftvar err error// Check for MD5snumberLeft.MD5s, err = ProcessMD5s(store, gpart, 0, ctx)if err != nil {return numberLeft, err}// Check for MetadatanumberLeft.Metadata, err = ProcessMetadata(store, gpart, 0)if err != nil {return numberLeft, err}// Check for PreviewsnumberLeft.Previews, err = ProcessPreviews(store, gpart, 0)if err != nil {return numberLeft, err}// Check for BlurhashesnumberLeft.Blurhashes, err = ProcessBlurHashes(store, gpart, 0)if err != nil {return numberLeft, err}numberLeft.CalculateMoreToProcess()return numberLeft, nil}func (numberLeft *NumberLeft) CalculateMoreToProcess() {numberLeft.MoreToProcess = 0left := [4]int{numberLeft.MD5s, numberLeft.Metadata, numberLeft.Previews, numberLeft.Blurhashes}for _, n := range left {if n > numberLeft.MoreToProcess {numberLeft.MoreToProcess = n}}}
package assetStoreimport ("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 stringconst (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) - batchLimitplength := batchLimitif 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 = <-resultsif 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 folderpreviewsPath := viper.GetString("PreviewsLocation")err := os.MkdirAll(previewsPath, 0755)if err != nil {log.Error().Err(err).Caller().Msg("Error creating previews directory")results <- errreturn}// Lookup Assetasset, err := photo.GetAsset(gm, gpart)if err != nil {log.Error().Err(err).Caller().Msg("Error getting asset.")results <- errreturn}if asset.Orphan {results <- nilreturn}//download the assetlog.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 <- errreturn}image, err := vips.NewImageFromFile(file)if err != nil {results <- handlePreviewError(photo, err, gm, gpart)return}// Rotate the picture upright and reset EXIF orientation tagerr = 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 = <-errsif err != nil {log.Error().Err(err).Caller().Msg("Processing previews")results <- errreturn}}photo.ProcessedPreviews = "true"node, err := photo.GetUpsertNode(gm, gpart)if err != nil {log.Error().Err(err).Caller().Msg("Error getting node for photo")results <- errreturn}err = trans.StoreNode(gpart, node)if err != nil {log.Error().Err(err).Caller().Msg("Error storing node for photo")results <- errreturn}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 <- errreturn}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 <- nilreturncase 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 <- nilreturncase 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 <- nilreturncase Full:image.Thumbnail(1000, 1000, vips.InterestingNone)ep := vips.NewDefaultWEBPExportParams()ep.Quality = 90ep.Lossless = falseep.Compression = 80imagebytes, _, 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 <- nilreturn}}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 assetStoreimport ("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 filesgraphAssets, 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 systemorphansFound := 0// for each graph asset, check to see if it exists in the storep, _ := pbar.Pbar(len(graphAssets))p.SetDescription("Checking for Orphans")p.Initialize()for _, graphAsset := range graphAssets {found := falsevar 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 = truebreak}}if !found {// If they don't, then we have an orphanlog.Debug().Msgf("O R P H A N !: %s", graphAsset.ETag)graphAsset.Orphan = trueerr := 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 assetStoreimport ("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) - batchLimitplength := batchLimitif 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 = <-resultsif 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.Photovar err errorvar month intvar year intlog.Info().Msgf("Processing asset %s", asset.GraphNode.Key)if asset.Orphan {results <- nilreturn}if !tryReprocess && asset.ProcessedMetadata != "false" {results <- nilreturn}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 = 1continuecase "February":month = 2continuecase "March":month = 3continuecase "April":month = 4continuecase "May":month = 5continuecase "June":month = 6continuecase "July":month = 7continuecase "August":month = 8continuecase "September":month = 9continuecase "October":month = 10continuecase "November":month = 11continuecase "December":month = 12continue}i, _ := strconv.ParseInt(part, 10, 64)if i > 1900 && i < 2100 { //i will be zero for non-numeric stringsyear = 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 <- errreturn}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 <- errreturn}err = trans.StoreNode(gpart, yearNode)if err != nil {log.Error().Err(err).Caller().Msg("Error storing year node on transaction.")results <- errreturn}err = trans.StoreEdge(gpart, yearEdge)if err != nil {log.Error().Err(err).Caller().Msg("Error storing year edge on transaction.")results <- errreturn}}if month != 0 {monthEdge, err := data.TakenInMonth(month, photo.GraphNode)if err != nil {log.Error().Err(err).Caller().Msg("Error getting month node.")results <- errreturn}err = trans.StoreEdge(gpart, monthEdge)if err != nil {log.Error().Err(err).Caller().Msg("Error storing month edge on transaction.")results <- errreturn}}asset.ProcessedMetadata = "true"assetNode, err := asset.GetUpsertNode(gm, gpart)if err != nil {log.Error().Err(err).Caller().Msg("Error getting upsert assest node")results <- errreturn}assetEdge, err := asset.GetTypeEdge(gm, gpart, photo.GraphNode)if err != nil {log.Error().Err(err).Caller().Msg("Error getting asset type edge")results <- errreturn}err = trans.StoreEdge(gpart, assetEdge)if err != nil {log.Error().Err(err).Caller().Msg("Error storing asset edge on transaction.")results <- errreturn}err = trans.StoreNode(gpart, assetNode)if err != nil {log.Error().Err(err).Caller().Msg("Error storing asset node on transaction.")results <- errreturn}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 ECIFrawExif, 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 Tagsim, 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:SSphoto.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.Altitudereturn 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 assetStoreimport ("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) - batchLimitplength := batchLimitif 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 = <-resultsif 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 <- nilreturn}filePath, err := Download(store, asset)if err != nil {results <- nilreturn}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 <- nilreturn}err = trans.StoreNode(gpart, upsertNode)if err != nil {results <- nilreturn}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 assetStoreimport ("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 TempDirfunc 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 + "." + extentionif _, 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 fileif 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) * 100fmt.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 fileif 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 assetStoreimport ("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_keypairs.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:portpairs.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-1pairs.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 assetStoreimport ("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 assetStoreimport ("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 assetStoreimport ("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) - batchLimitplength := batchLimitif 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 = <-resultsif 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 existsimageFile, 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.Imageimg, 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## HashesHash 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 DataEXIF 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## PreviewsFor each asset we generate a preview asset, and store that previews location in the graph.### Photo PreviewsFor 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 HashTo 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).## DatesCreation 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 PlacesPlaces 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)).- - -### NotesTo add a new command line command```bash$HOME/go/bin/cobra add {name of command}```