diff --git a/internal/database/chapter.go b/internal/database/chapter.go index fd627ea..54a98ce 100644 --- a/internal/database/chapter.go +++ b/internal/database/chapter.go @@ -8,9 +8,10 @@ type Chapter struct { Number string TimeStampUnix int64 MangaId int + UserId int } -func NewChapter(id int, mangaId int, url string, name string, number string, timeStampUnix int64) Chapter { +func NewChapter(id int, mangaId int, userId int, url string, name string, number string, timeStampUnix int64) Chapter { return Chapter{ ChapterId: id, Url: url, @@ -18,5 +19,6 @@ func NewChapter(id int, mangaId int, url string, name string, number string, tim Number: number, TimeStampUnix: timeStampUnix, MangaId: mangaId, + UserId: userId, } } diff --git a/internal/database/manga.go b/internal/database/manga.go index 26b3c88..3117b79 100644 --- a/internal/database/manga.go +++ b/internal/database/manga.go @@ -3,7 +3,6 @@ package database type MangaDefinition struct { Id int `gorm:"primary_key;AUTO_INCREMENT"` Title string - TimeStampUnix int64 Thumbnail []byte LastChapterNum string // Chapters []Chapter @@ -13,21 +12,31 @@ type MangaDefinition struct { type Manga struct { Id int `gorm:"primary_key;AUTO_INCREMENT"` MangaDefinitionId int - Definition MangaDefinition `gorm:"foreignKey:MangaDefinitionId"` + Definition MangaDefinition `gorm:"foreignKey:MangaDefinitionId"` UserId int + User User TimeStampUnix int64 Chapters []Chapter `gorm:"foreignKey:MangaId"` } -func NewMangaDefinition(id int, title string, timeStampUnix int64) MangaDefinition { +func NewMangaDefinition(id int, title string) MangaDefinition { return MangaDefinition{ Id: id, Title: title, - TimeStampUnix: timeStampUnix, LastChapterNum: "", } } +func NewManga(def MangaDefinition, user User, timeStampUnix int64) Manga { + return Manga{ + MangaDefinitionId: def.Id, + Definition: def, + UserId: user.Id, + User: user, + TimeStampUnix: timeStampUnix, + } +} + // GetLatestChapter TODO: Cache this somehow func (m *Manga) GetLatestChapter() (*Chapter, bool) { // highest := int64(0) diff --git a/internal/database/user.go b/internal/database/user.go index deadb5b..9fef4a5 100644 --- a/internal/database/user.go +++ b/internal/database/user.go @@ -6,7 +6,6 @@ type User struct { LoginName string PwdHash []byte Salt []byte - Mangas []Manga `gorm:"foreignKey:UserId"` } // type UserManga struct { diff --git a/internal/server/handler.go b/internal/server/handler.go index 03f8581..d5be3e2 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -5,40 +5,112 @@ import ( _ "embed" "errors" "fmt" - "github.com/pablu23/mangaGetter/internal/database" - "github.com/pablu23/mangaGetter/internal/view" - "golang.org/x/text/cases" - "golang.org/x/text/language" - "gorm.io/gorm" "html/template" "net/http" "slices" "strconv" "strings" "time" + + "github.com/pablu23/mangaGetter/internal/database" + "github.com/pablu23/mangaGetter/internal/view" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "gorm.io/gorm" ) +func (s *Server) getSessionFromCookie(w http.ResponseWriter, r *http.Request) (*UserSession, error) { + cookie, err := r.Cookie("session") + if err != nil { + switch { + case errors.Is(err, http.ErrNoCookie): + // http.Error(w, "cookie not found", http.StatusBadRequest) + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + default: + fmt.Println(err) + http.Error(w, "server error", http.StatusInternalServerError) + } + } + session, ok := s.Sessions[cookie.Value] + if !ok { + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) + return nil, errors.New("Unknown Session") + } + return session, err +} + +func (s *Server) HandleRegister(w http.ResponseWriter, r *http.Request) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + admin := database.User{ + Id: 1, + DisplayName: "admin", + LoginName: "admin", + } + s.DbMgr.Db.Create(&admin) + + http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) +} + +func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) { + s.Mutex.Lock() + defer s.Mutex.Unlock() + + // Login + s.Sessions["abcd"] = &UserSession{ + User: database.User{ + Id: 1, + DisplayName: "admin", + LoginName: "admin", + }, + } + + cookie := http.Cookie{ + Name: "session", + Value: "abcd", + Path: "/", + MaxAge: 3600, + Secure: true, + HttpOnly: false, + SameSite: http.SameSiteLaxMode, + } + + http.SetCookie(w, &cookie) + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + func (s *Server) HandleNew(w http.ResponseWriter, r *http.Request) { title := r.PathValue("title") chapter := r.PathValue("chapter") url := fmt.Sprintf("/title/%s/%s", title, chapter) - s.CurrSubUrl = url - s.PrevSubUrl = "" - s.NextSubUrl = "" - s.LoadCurr() + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } - go s.LoadNext() - go s.LoadPrev() + session.CurrSubUrl = url + session.PrevSubUrl = "" + session.NextSubUrl = "" + s.LoadCurr(session) + + go s.LoadNext(session) + go s.LoadPrev(session) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect) } -func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) { +func (s *Server) HandleMenu(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(view.GetViewTemplate(view.Menu)) + + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } var all []*database.Manga - _ = s.DbMgr.Db.Preload("Chapters").Where("user_id = ?", 1).Find(&all) + _ = s.DbMgr.Db.Preload("Chapters").Where("user_id = ?", session.User.Id).Find(&all) l := len(all) mangaViewModels := make([]view.MangaViewModel, l) counter := 0 @@ -148,7 +220,7 @@ func (s *Server) HandleMenu(w http.ResponseWriter, _ *http.Request) { Mangas: mangaViewModels, } - err := tmpl.Execute(w, menuViewModel) + err = tmpl.Execute(w, menuViewModel) if err != nil { fmt.Println(err) } @@ -177,63 +249,77 @@ func (s *Server) HandleDelete(w http.ResponseWriter, r *http.Request) { func (s *Server) HandleExit(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + // session, err := s.getSessionFromCookie(w, r) + // if err != nil { + // return + // } + go func() { - s.Mutex.Lock() - if s.PrevViewModel != nil { - for _, img := range s.PrevViewModel.Images { - delete(s.ImageBuffers, img.Path) - } - } - if s.CurrViewModel != nil { - - for _, img := range s.CurrViewModel.Images { - delete(s.ImageBuffers, img.Path) - } - } - if s.NextViewModel != nil { - - for _, img := range s.NextViewModel.Images { - delete(s.ImageBuffers, img.Path) - } - } - s.Mutex.Unlock() + // session.Mutex.Lock() + // if session.PrevViewModel != nil { + // for _, img := range session.PrevViewModel.Images { + // delete(s.ImageBuffers, img.Path) + // } + // } + // if session.CurrViewModel != nil { + // + // for _, img := range session.CurrViewModel.Images { + // delete(s.ImageBuffers, img.Path) + // } + // } + // if session.NextViewModel != nil { + // + // for _, img := range session.NextViewModel.Images { + // delete(s.ImageBuffers, img.Path) + // } + // } + // session.Mutex.Unlock() fmt.Println("Cleaned last Manga") }() } -func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) { +func (s *Server) HandleCurrent(w http.ResponseWriter, r *http.Request) { tmpl := template.Must(view.GetViewTemplate(view.Viewer)) - mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(s.CurrSubUrl) + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } + mangaId, chapterId, err := s.Provider.GetTitleIdAndChapterId(session.CurrSubUrl) if err != nil { fmt.Println(err) } - title, chapterName, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl) + title, chapterName, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl) if err != nil { fmt.Println(err) } - var manga database.MangaDefinition - result := s.DbMgr.Db.First(&manga, mangaId) + var mangaDef database.MangaDefinition + result := s.DbMgr.Db.First(&mangaDef, mangaId) if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { - manga = database.NewMangaDefinition(mangaId, title, time.Now().Unix()) - } else { - manga.TimeStampUnix = time.Now().Unix() + mangaDef = database.NewMangaDefinition(mangaId, title) + } + + var manga database.Manga + result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&manga, "manga_definition_id = ?", mangaId) + if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { + manga = database.NewManga(mangaDef, session.User, time.Now().Unix()) } var chapter database.Chapter - result = s.DbMgr.Db.First(&chapter, chapterId) + result = s.DbMgr.Db.Where("user_id = ?", session.User.Id).First(&chapter, "chapter_id = ?", chapterId) if result.Error != nil && errors.Is(result.Error, gorm.ErrRecordNotFound) { chapterNumberStr := strings.Replace(chapterName, "ch_", "", 1) - chapter = database.NewChapter(chapterId, mangaId, s.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix()) + chapter = database.NewChapter(chapterId, mangaId, session.User.Id, session.CurrSubUrl, chapterName, chapterNumberStr, time.Now().Unix()) } else { chapter.TimeStampUnix = time.Now().Unix() } + s.DbMgr.Db.Save(&mangaDef) s.DbMgr.Db.Save(&manga) s.DbMgr.Db.Save(&chapter) - err = tmpl.Execute(w, s.CurrViewModel) + err = tmpl.Execute(w, session.CurrViewModel) if err != nil { fmt.Println(err) } @@ -241,8 +327,8 @@ func (s *Server) HandleCurrent(w http.ResponseWriter, _ *http.Request) { func (s *Server) HandleImage(w http.ResponseWriter, r *http.Request) { u := r.PathValue("url") - s.Mutex.Lock() - defer s.Mutex.Unlock() + s.Mutex.RLock() + defer s.Mutex.RUnlock() buf := s.ImageBuffers[u] if buf == nil { fmt.Printf("url: %s is nil\n", u) @@ -271,7 +357,12 @@ func (s *Server) HandleFavicon(w http.ResponseWriter, _ *http.Request) { func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) { fmt.Println("Received Next") - if s.PrevViewModel != nil { + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } + + if session.PrevViewModel != nil { go func(viewModel view.ImageViewModel, s *Server) { s.Mutex.Lock() for _, img := range viewModel.Images { @@ -279,26 +370,31 @@ func (s *Server) HandleNext(w http.ResponseWriter, r *http.Request) { } s.Mutex.Unlock() fmt.Println("Cleaned out of scope Last") - }(*s.PrevViewModel, s) + }(*session.PrevViewModel, s) } - if s.NextViewModel == nil || s.NextSubUrl == "" { + if session.NextViewModel == nil || session.NextSubUrl == "" { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } - s.PrevViewModel = s.CurrViewModel - s.CurrViewModel = s.NextViewModel - s.PrevSubUrl = s.CurrSubUrl - s.CurrSubUrl = s.NextSubUrl + session.PrevViewModel = session.CurrViewModel + session.CurrViewModel = session.NextViewModel + session.PrevSubUrl = session.CurrSubUrl + session.CurrSubUrl = session.NextSubUrl - go s.LoadNext() + go s.LoadNext(session) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect) } func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) { fmt.Println("Received Prev") - if s.NextViewModel != nil { + + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } + if session.NextViewModel != nil { go func(viewModel view.ImageViewModel, s *Server) { s.Mutex.Lock() for _, img := range viewModel.Images { @@ -306,20 +402,20 @@ func (s *Server) HandlePrev(w http.ResponseWriter, r *http.Request) { } s.Mutex.Unlock() fmt.Println("Cleaned out of scope Last") - }(*s.NextViewModel, s) + }(*session.NextViewModel, s) } - if s.PrevViewModel == nil || s.PrevSubUrl == "" { + if session.PrevViewModel == nil || session.PrevSubUrl == "" { http.Redirect(w, r, "/", http.StatusTemporaryRedirect) return } - s.NextViewModel = s.CurrViewModel - s.CurrViewModel = s.PrevViewModel - s.NextSubUrl = s.CurrSubUrl - s.CurrSubUrl = s.PrevSubUrl + session.NextViewModel = session.CurrViewModel + session.CurrViewModel = session.PrevViewModel + session.NextSubUrl = session.CurrSubUrl + session.CurrSubUrl = session.PrevSubUrl - go s.LoadPrev() + go s.LoadPrev(session) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect) } @@ -363,13 +459,17 @@ func (s *Server) HandleNewQuery(w http.ResponseWriter, r *http.Request) { url := fmt.Sprintf("/title/%s", sub) - s.CurrSubUrl = url - s.PrevSubUrl = "" - s.NextSubUrl = "" - s.LoadCurr() + session, err := s.getSessionFromCookie(w, r) + if err != nil { + return + } + session.CurrSubUrl = url + session.PrevSubUrl = "" + session.NextSubUrl = "" + s.LoadCurr(session) - go s.LoadNext() - go s.LoadPrev() + go s.LoadNext(session) + go s.LoadPrev(session) http.Redirect(w, r, "/current/", http.StatusTemporaryRedirect) } diff --git a/internal/server/server.go b/internal/server/server.go index 582b3f8..4ed5ce3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,50 +4,57 @@ import ( "bytes" _ "embed" "fmt" - "github.com/pablu23/mangaGetter/internal/database" - "github.com/pablu23/mangaGetter/internal/provider" - "github.com/pablu23/mangaGetter/internal/view" "io" "net/http" "path/filepath" "strconv" "strings" "sync" - "time" + + "github.com/pablu23/mangaGetter/internal/database" + "github.com/pablu23/mangaGetter/internal/provider" + "github.com/pablu23/mangaGetter/internal/view" ) type Server struct { + ImageBuffers map[string][]byte + Provider provider.Provider + DbMgr *database.Manager + + Mutex *sync.RWMutex + + Sessions map[string]*UserSession +} + +type UserSession struct { + User database.User + + // Mutex *sync.Mutex + + PrevSubUrl string + CurrSubUrl string + NextSubUrl string + PrevViewModel *view.ImageViewModel CurrViewModel *view.ImageViewModel NextViewModel *view.ImageViewModel - - ImageBuffers map[string][]byte - Mutex *sync.Mutex - - NextSubUrl string - CurrSubUrl string - PrevSubUrl string - - Provider provider.Provider - - IsFirst bool - IsLast bool - - DbMgr *database.Manager } func New(provider provider.Provider, db *database.Manager) *Server { s := Server{ ImageBuffers: make(map[string][]byte), + Sessions: make(map[string]*UserSession), Provider: provider, DbMgr: db, - Mutex: &sync.Mutex{}, + Mutex: &sync.RWMutex{}, } return &s } func (s *Server) Start(port int) error { + http.HandleFunc("/register", s.HandleRegister) + http.HandleFunc("/login", s.HandleLogin) http.HandleFunc("/", s.HandleMenu) http.HandleFunc("/new/", s.HandleNewQuery) http.HandleFunc("/new/title/{title}/{chapter}", s.HandleNew) @@ -62,74 +69,66 @@ func (s *Server) Start(port int) error { http.HandleFunc("GET /setting/set/{setting}/{value}", s.HandleSettingSet) // Update Latest Chapters every 5 Minutes - go func(s *Server) { - time.AfterFunc(time.Second*10, func() { - var all []*database.Manga - s.DbMgr.Db.Find(&all) - for _, m := range all { - err, updated := s.UpdateLatestAvailableChapter(m) - if err != nil { - fmt.Println(err) - } - if updated { - s.DbMgr.Db.Save(m) - } - } - }) - - for { - select { - case <-time.After(time.Minute * 5): - var all []*database.Manga - s.DbMgr.Db.Find(&all) - for _, m := range all { - err, updated := s.UpdateLatestAvailableChapter(m) - if err != nil { - fmt.Println(err) - } - if updated { - s.DbMgr.Db.Save(m) - } - } - } - } - }(s) - + // go func(s *Server) { + // time.AfterFunc(time.Second*10, func() { + // var all []*database.Manga + // s.DbMgr.Db.Find(&all) + // for _, m := range all { + // err, updated := s.UpdateLatestAvailableChapter(m) + // if err != nil { + // fmt.Println(err) + // } + // if updated { + // s.DbMgr.Db.Save(m) + // } + // } + // }) + // + // for { + // select { + // case <-time.After(time.Minute * 5): + // var all []*database.Manga + // s.DbMgr.Db.Find(&all) + // for _, m := range all { + // err, updated := s.UpdateLatestAvailableChapter(m) + // if err != nil { + // fmt.Println(err) + // } + // if updated { + // s.DbMgr.Db.Save(m) + // } + // } + // } + // } + // }(s) + // fmt.Println("Server starting...") err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil) return err } -func (s *Server) LoadNext() { - c, err := s.Provider.GetHtml(s.CurrSubUrl) +func (s *Server) LoadNext(session *UserSession) { + next, err := s.Provider.GetHtml(session.CurrSubUrl) if err != nil { fmt.Println(err) - s.NextSubUrl = "" - s.NextViewModel = nil - return - } - - next, err := s.Provider.GetNext(c) - if err != nil { - fmt.Println(err) - s.NextSubUrl = "" - s.NextViewModel = nil + session.NextSubUrl = "" + session.NextViewModel = nil return } html, err := s.Provider.GetHtml(next) if err != nil { fmt.Println(err) - s.NextSubUrl = "" - s.NextViewModel = nil + session.NextSubUrl = "" + session.NextViewModel = nil return } imagesNext, err := s.AppendImagesToBuf(html) if err != nil { fmt.Println(err) - s.NextSubUrl = "" - s.NextViewModel = nil + session.NextSubUrl = "" + session.NextViewModel = nil return } @@ -141,39 +140,39 @@ func (s *Server) LoadNext() { full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) - s.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} - s.NextSubUrl = next + session.NextViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} + session.NextSubUrl = next fmt.Println("Loaded next") } -func (s *Server) LoadPrev() { - c, err := s.Provider.GetHtml(s.CurrSubUrl) +func (s *Server) LoadPrev(session *UserSession) { + c, err := s.Provider.GetHtml(session.CurrSubUrl) if err != nil { fmt.Println(err) - s.PrevSubUrl = "" - s.PrevViewModel = nil + session.PrevSubUrl = "" + session.PrevViewModel = nil return } prev, err := s.Provider.GetPrev(c) if err != nil { fmt.Println(err) - s.PrevSubUrl = "" - s.PrevViewModel = nil + session.PrevSubUrl = "" + session.PrevViewModel = nil return } html, err := s.Provider.GetHtml(prev) if err != nil { fmt.Println(err) - s.PrevSubUrl = "" - s.PrevViewModel = nil + session.PrevSubUrl = "" + session.PrevViewModel = nil return } imagesNext, err := s.AppendImagesToBuf(html) if err != nil { fmt.Println(err) - s.PrevSubUrl = "" - s.PrevViewModel = nil + session.PrevSubUrl = "" + session.PrevViewModel = nil return } @@ -185,21 +184,21 @@ func (s *Server) LoadPrev() { full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) - s.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} + session.PrevViewModel = &view.ImageViewModel{Images: imagesNext, Title: full} - s.PrevSubUrl = prev + session.PrevSubUrl = prev fmt.Println("Loaded prev") } -func (s *Server) LoadCurr() { - html, err := s.Provider.GetHtml(s.CurrSubUrl) +func (s *Server) LoadCurr(session *UserSession) { + html, err := s.Provider.GetHtml(session.CurrSubUrl) if err != nil { panic(err) } imagesCurr, err := s.AppendImagesToBuf(html) - title, chapter, err := s.Provider.GetTitleAndChapter(s.CurrSubUrl) + title, chapter, err := s.Provider.GetTitleAndChapter(session.CurrSubUrl) if err != nil { title = "Unknown" chapter = "ch_?" @@ -207,7 +206,7 @@ func (s *Server) LoadCurr() { full := strings.Replace(title, "-", " ", -1) + " - " + strings.Replace(chapter, "_", " ", -1) - s.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full} + session.CurrViewModel = &view.ImageViewModel{Images: imagesCurr, Title: full} fmt.Println("Loaded current") } diff --git a/internal/utils/concurrent_map.go b/internal/utils/concurrent_map.go new file mode 100644 index 0000000..abf5014 --- /dev/null +++ b/internal/utils/concurrent_map.go @@ -0,0 +1,31 @@ +package utils + +import "sync" + +type ConcurrentMap[K comparable, Value any] struct { + dirty map[K]Value + count int + mutex *sync.RWMutex +} + +func NewConcurrentMap[K comparable, V any]() ConcurrentMap[K, V] { + return ConcurrentMap[K, V]{ + dirty: make(map[K]V), + count: 0, + mutex: &sync.RWMutex{}, + } +} + +func (c *ConcurrentMap[K, Value]) Get(key K) (Value, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + val, ok := c.dirty[key] + + return val, ok +} + +func (c *ConcurrentMap[K, Value]) Set(key K, val Value) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.dirty[key] = val +} diff --git a/mangaGetter b/mangaGetter index a03303d..e2d196d 100755 Binary files a/mangaGetter and b/mangaGetter differ