Merge branch 'master' of github.com:ewhal/nyaa into api
Cette révision appartient à :
révision
af2c47c2f2
|
@ -7,9 +7,9 @@ before_install:
|
||||||
script:
|
script:
|
||||||
# Downloads deps automatically. No need to add manually.
|
# Downloads deps automatically. No need to add manually.
|
||||||
- go list -f '{{.Deps}}' | tr "[" " " | tr "]" " " | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -v 'github.com/ewhal/nyaa' | xargs go get -v
|
- go list -f '{{.Deps}}' | tr "[" " " | tr "]" " " | xargs go list -e -f '{{if not .Standard}}{{.ImportPath}}{{end}}' | grep -v 'github.com/ewhal/nyaa' | xargs go get -v
|
||||||
- go get
|
- go get
|
||||||
- go build
|
- go build
|
||||||
- go vet
|
- go vet
|
||||||
- go test -v ./...
|
- go test -v ./...
|
||||||
before_deploy:
|
before_deploy:
|
||||||
- ./package.sh
|
- ./package.sh
|
||||||
|
|
15
README.md
15
README.md
|
@ -64,17 +64,18 @@ Access the website by going to [localhost:9999](http://localhost:9999).
|
||||||
> nyaa_psql.backup.
|
> nyaa_psql.backup.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
* improve scraping
|
* sukebei
|
||||||
* fix up cache bug
|
* get sukebei_torrents table working
|
||||||
* import sukebei torrents into db/work on sukebei
|
* add config option for sukebei or maybe make things all in one
|
||||||
|
* sukebei categories and category images
|
||||||
|
|
||||||
* Get code up to standard of go lint recommendations
|
* Get code up to standard of go lint recommendations
|
||||||
* Write tests
|
* Write tests
|
||||||
* fix sukebei categories
|
|
||||||
* Daily DB dumps
|
* Site theme
|
||||||
* Site theme
|
|
||||||
* original nyaa theme
|
* original nyaa theme
|
||||||
* API improvement
|
* API improvement
|
||||||
* Scraping of fan subbing RSS feeds
|
* Scraping of fan subbing RSS feeds(WIP)
|
||||||
|
|
||||||
# LICENSE
|
# LICENSE
|
||||||
This project is licensed under the MIT License - see the LICENSE.md file for details
|
This project is licensed under the MIT License - see the LICENSE.md file for details
|
||||||
|
|
139
cache/cache.go
externe
139
cache/cache.go
externe
|
@ -1,132 +1,37 @@
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"github.com/ewhal/nyaa/cache/memcache"
|
||||||
"sync"
|
"github.com/ewhal/nyaa/cache/native"
|
||||||
"time"
|
"github.com/ewhal/nyaa/cache/nop"
|
||||||
|
|
||||||
"github.com/ewhal/nyaa/common"
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/config"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
|
|
||||||
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
const expiryTime = time.Minute
|
// Cache defines interface for caching search results
|
||||||
|
type Cache interface {
|
||||||
var (
|
Get(key common.SearchParam, r func() ([]model.Torrent, int, error)) ([]model.Torrent, int, error)
|
||||||
cache = make(map[common.SearchParam]*list.Element, 10)
|
ClearAll()
|
||||||
ll = list.New()
|
|
||||||
totalUsed int
|
|
||||||
mu sync.Mutex
|
|
||||||
|
|
||||||
// Size sets the maximum size of the cache before evicting unread data in MB
|
|
||||||
Size float64 = 1 << 10
|
|
||||||
)
|
|
||||||
|
|
||||||
// Key stores the ID of either a thread or board page
|
|
||||||
type Key struct {
|
|
||||||
LastN uint8
|
|
||||||
Board string
|
|
||||||
ID uint64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single cache entry
|
var ErrInvalidCacheDialect = errors.New("invalid cache dialect")
|
||||||
type store struct {
|
|
||||||
sync.Mutex // Controls general access to the contents of the struct
|
|
||||||
lastFetched time.Time
|
|
||||||
key common.SearchParam
|
|
||||||
data []model.Torrent
|
|
||||||
count, size int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the cache for and existing record. If miss, run fn to retrieve fresh
|
// Impl cache implementation instance
|
||||||
// values.
|
var Impl Cache
|
||||||
func Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) (
|
|
||||||
data []model.Torrent, count int, err error,
|
|
||||||
) {
|
|
||||||
s := getStore(key)
|
|
||||||
|
|
||||||
// Also keeps multiple requesters from simultaneously requesting the same
|
func Configure(conf *config.CacheConfig) (err error) {
|
||||||
// data
|
switch conf.Dialect {
|
||||||
s.Lock()
|
case "native":
|
||||||
defer s.Unlock()
|
Impl = native.New(conf.Size)
|
||||||
|
|
||||||
if s.isFresh() {
|
|
||||||
return s.data, s.count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
data, count, err = fn()
|
|
||||||
if err != nil {
|
|
||||||
return
|
return
|
||||||
|
case "memcache":
|
||||||
|
Impl = memcache.New()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
Impl = nop.New()
|
||||||
}
|
}
|
||||||
s.update(data, count)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve a store from the cache or create a new one
|
|
||||||
func getStore(k common.SearchParam) (s *store) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
el := cache[k]
|
|
||||||
if el == nil {
|
|
||||||
s = &store{key: k}
|
|
||||||
cache[k] = ll.PushFront(s)
|
|
||||||
} else {
|
|
||||||
ll.MoveToFront(el)
|
|
||||||
s = el.Value.(*store)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the cache. Only used for testing.
|
|
||||||
func Clear() {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
ll = list.New()
|
|
||||||
cache = make(map[common.SearchParam]*list.Element, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the total used memory counter and evict, if over limit
|
|
||||||
func updateUsedSize(delta int) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
totalUsed += delta
|
|
||||||
|
|
||||||
for totalUsed > int(Size)<<20 {
|
|
||||||
e := ll.Back()
|
|
||||||
if e == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
s := ll.Remove(e).(*store)
|
|
||||||
delete(cache, s.key)
|
|
||||||
totalUsed -= s.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return, if the data can still be considered fresh, without querying the DB
|
|
||||||
func (s *store) isFresh() bool {
|
|
||||||
if s.lastFetched.IsZero() { // New store
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return s.lastFetched.Add(expiryTime).After(time.Now())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stores the new values of s. Calculates and stores the new size. Passes the
|
|
||||||
// delta to the central cache to fire eviction checks.
|
|
||||||
func (s *store) update(data []model.Torrent, count int) {
|
|
||||||
newSize := 0
|
|
||||||
for _, d := range data {
|
|
||||||
newSize += d.Size()
|
|
||||||
}
|
|
||||||
s.data = data
|
|
||||||
s.count = count
|
|
||||||
delta := newSize - s.size
|
|
||||||
s.size = newSize
|
|
||||||
s.lastFetched = time.Now()
|
|
||||||
|
|
||||||
// Technically it is possible to update the size even when the store is
|
|
||||||
// already evicted, but that should never happen, unless you have a very
|
|
||||||
// small cache, very large stored datasets and a lot of traffic.
|
|
||||||
updateUsedSize(delta)
|
|
||||||
}
|
|
||||||
|
|
21
cache/memcache/memcache.go
externe
Fichier normal
21
cache/memcache/memcache.go
externe
Fichier normal
|
@ -0,0 +1,21 @@
|
||||||
|
package memcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Memcache struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Memcache) Get(key common.SearchParam, r func() ([]model.Torrent, int, error)) (torrents []model.Torrent, num int, err error) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Memcache) ClearAll() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Memcache {
|
||||||
|
return &Memcache{}
|
||||||
|
}
|
143
cache/native/native.go
externe
Fichier normal
143
cache/native/native.go
externe
Fichier normal
|
@ -0,0 +1,143 @@
|
||||||
|
package native
|
||||||
|
|
||||||
|
import (
|
||||||
|
"container/list"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const expiryTime = time.Minute
|
||||||
|
|
||||||
|
// NativeCache implements cache.Cache
|
||||||
|
type NativeCache struct {
|
||||||
|
cache map[common.SearchParam]*list.Element
|
||||||
|
ll *list.List
|
||||||
|
totalUsed int
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// Size sets the maximum size of the cache before evicting unread data in MB
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New Creates New Native Cache instance
|
||||||
|
func New(sz float64) *NativeCache {
|
||||||
|
return &NativeCache{
|
||||||
|
cache: make(map[common.SearchParam]*list.Element, 10),
|
||||||
|
Size: sz,
|
||||||
|
ll: list.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key stores the ID of either a thread or board page
|
||||||
|
type Key struct {
|
||||||
|
LastN uint8
|
||||||
|
Board string
|
||||||
|
ID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single cache entry
|
||||||
|
type store struct {
|
||||||
|
sync.Mutex // Controls general access to the contents of the struct
|
||||||
|
lastFetched time.Time
|
||||||
|
key common.SearchParam
|
||||||
|
data []model.Torrent
|
||||||
|
count, size int
|
||||||
|
n *NativeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the cache for and existing record. If miss, run fn to retrieve fresh
|
||||||
|
// values.
|
||||||
|
func (n *NativeCache) Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) (
|
||||||
|
data []model.Torrent, count int, err error,
|
||||||
|
) {
|
||||||
|
s := n.getStore(key)
|
||||||
|
|
||||||
|
// Also keeps multiple requesters from simultaneously requesting the same
|
||||||
|
// data
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if s.isFresh() {
|
||||||
|
return s.data, s.count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, count, err = fn()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.update(data, count)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve a store from the cache or create a new one
|
||||||
|
func (n *NativeCache) getStore(k common.SearchParam) (s *store) {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
el := n.cache[k]
|
||||||
|
if el == nil {
|
||||||
|
s = &store{key: k, n: n}
|
||||||
|
n.cache[k] = n.ll.PushFront(s)
|
||||||
|
} else {
|
||||||
|
n.ll.MoveToFront(el)
|
||||||
|
s = el.Value.(*store)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cache. Only used for testing.
|
||||||
|
func (n *NativeCache) ClearAll() {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
n.ll = list.New()
|
||||||
|
n.cache = make(map[common.SearchParam]*list.Element, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the total used memory counter and evict, if over limit
|
||||||
|
func (n *NativeCache) updateUsedSize(delta int) {
|
||||||
|
n.mu.Lock()
|
||||||
|
defer n.mu.Unlock()
|
||||||
|
|
||||||
|
n.totalUsed += delta
|
||||||
|
|
||||||
|
for n.totalUsed > int(n.Size)<<20 {
|
||||||
|
e := n.ll.Back()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s := n.ll.Remove(e).(*store)
|
||||||
|
delete(n.cache, s.key)
|
||||||
|
n.totalUsed -= s.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return, if the data can still be considered fresh, without querying the DB
|
||||||
|
func (s *store) isFresh() bool {
|
||||||
|
if s.lastFetched.IsZero() { // New store
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return s.lastFetched.Add(expiryTime).After(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stores the new values of s. Calculates and stores the new size. Passes the
|
||||||
|
// delta to the central cache to fire eviction checks.
|
||||||
|
func (s *store) update(data []model.Torrent, count int) {
|
||||||
|
newSize := 0
|
||||||
|
for _, d := range data {
|
||||||
|
newSize += d.Size()
|
||||||
|
}
|
||||||
|
s.data = data
|
||||||
|
s.count = count
|
||||||
|
delta := newSize - s.size
|
||||||
|
s.size = newSize
|
||||||
|
s.lastFetched = time.Now()
|
||||||
|
|
||||||
|
// Technically it is possible to update the size even when the store is
|
||||||
|
// already evicted, but that should never happen, unless you have a very
|
||||||
|
// small cache, very large stored datasets and a lot of traffic.
|
||||||
|
s.n.updateUsedSize(delta)
|
||||||
|
}
|
22
cache/nop/nop.go
externe
Fichier normal
22
cache/nop/nop.go
externe
Fichier normal
|
@ -0,0 +1,22 @@
|
||||||
|
package nop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NopCache struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NopCache) Get(key common.SearchParam, fn func() ([]model.Torrent, int, error)) ([]model.Torrent, int, error) {
|
||||||
|
return fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NopCache) ClearAll() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Cache that does NOTHING :D
|
||||||
|
func New() *NopCache {
|
||||||
|
return &NopCache{}
|
||||||
|
}
|
|
@ -19,6 +19,9 @@ const (
|
||||||
Date
|
Date
|
||||||
Downloads
|
Downloads
|
||||||
Size
|
Size
|
||||||
|
Seeders
|
||||||
|
Leechers
|
||||||
|
Completed
|
||||||
)
|
)
|
||||||
|
|
||||||
type Category struct {
|
type Category struct {
|
||||||
|
@ -44,5 +47,6 @@ type SearchParam struct {
|
||||||
Page int
|
Page int
|
||||||
UserID uint
|
UserID uint
|
||||||
Max uint
|
Max uint
|
||||||
|
NotNull string
|
||||||
Query string
|
Query string
|
||||||
}
|
}
|
||||||
|
|
14
config/cache.go
Fichier normal
14
config/cache.go
Fichier normal
|
@ -0,0 +1,14 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
// CacheConfig is config struct for caching strategy
|
||||||
|
type CacheConfig struct {
|
||||||
|
Dialect string
|
||||||
|
URL string
|
||||||
|
Size float64
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCacheSize = 1 << 10
|
||||||
|
|
||||||
|
var DefaultCacheConfig = CacheConfig{
|
||||||
|
Dialect: "nop",
|
||||||
|
}
|
|
@ -22,13 +22,18 @@ type Config struct {
|
||||||
// DBParams will be directly passed to Gorm, and its internal
|
// DBParams will be directly passed to Gorm, and its internal
|
||||||
// structure depends on the dialect for each db type
|
// structure depends on the dialect for each db type
|
||||||
DBParams string `json:"db_params"`
|
DBParams string `json:"db_params"`
|
||||||
|
DBLogMode string `json:"db_logmode"`
|
||||||
// tracker scraper config (required)
|
// tracker scraper config (required)
|
||||||
Scrape ScraperConfig `json:"scraper"`
|
Scrape ScraperConfig `json:"scraper"`
|
||||||
|
// cache config
|
||||||
|
Cache CacheConfig `json:"cache"`
|
||||||
|
// search config
|
||||||
|
Search SearchConfig `json:"search"`
|
||||||
// optional i2p configuration
|
// optional i2p configuration
|
||||||
I2P *I2PConfig `json:"i2p"`
|
I2P *I2PConfig `json:"i2p"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var Defaults = Config{"localhost", 9999, "sqlite3", "./nyaa.db?cache_size=50", DefaultScraperConfig, nil}
|
var Defaults = Config{"localhost", 9999, "sqlite3", "./nyaa.db?cache_size=50", "default", DefaultScraperConfig, DefaultCacheConfig, DefaultSearchConfig, nil}
|
||||||
|
|
||||||
var allowedDatabaseTypes = map[string]bool{
|
var allowedDatabaseTypes = map[string]bool{
|
||||||
"sqlite3": true,
|
"sqlite3": true,
|
||||||
|
@ -37,13 +42,21 @@ var allowedDatabaseTypes = map[string]bool{
|
||||||
"mssql": true,
|
"mssql": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var allowedDBLogModes = map[string]bool{
|
||||||
|
"default": true, // errors only
|
||||||
|
"detailed": true,
|
||||||
|
"silent": true,
|
||||||
|
}
|
||||||
|
|
||||||
func New() *Config {
|
func New() *Config {
|
||||||
var config Config
|
var config Config
|
||||||
config.Host = Defaults.Host
|
config.Host = Defaults.Host
|
||||||
config.Port = Defaults.Port
|
config.Port = Defaults.Port
|
||||||
config.DBType = Defaults.DBType
|
config.DBType = Defaults.DBType
|
||||||
config.DBParams = Defaults.DBParams
|
config.DBParams = Defaults.DBParams
|
||||||
|
config.DBLogMode = Defaults.DBLogMode
|
||||||
config.Scrape = Defaults.Scrape
|
config.Scrape = Defaults.Scrape
|
||||||
|
config.Cache = Defaults.Cache
|
||||||
return &config
|
return &config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +68,7 @@ func (config *Config) BindFlags() func() error {
|
||||||
host := flag.String("host", Defaults.Host, "binding address of the server")
|
host := flag.String("host", Defaults.Host, "binding address of the server")
|
||||||
port := flag.Int("port", Defaults.Port, "port of the server")
|
port := flag.Int("port", Defaults.Port, "port of the server")
|
||||||
dbParams := flag.String("dbparams", Defaults.DBParams, "parameters to open the database (see Gorm's doc)")
|
dbParams := flag.String("dbparams", Defaults.DBParams, "parameters to open the database (see Gorm's doc)")
|
||||||
|
dbLogMode := flag.String("dblogmode", Defaults.DBLogMode, "database log verbosity (errors only by default)")
|
||||||
|
|
||||||
return func() error {
|
return func() error {
|
||||||
// You can override fields in the config file with flags.
|
// You can override fields in the config file with flags.
|
||||||
|
@ -65,6 +79,10 @@ func (config *Config) BindFlags() func() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = config.SetDBLogMode(*dbLogMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = config.HandleConfFileFlag(*confFile)
|
err = config.HandleConfFileFlag(*confFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -93,6 +111,14 @@ func (config *Config) SetDBType(db_type string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (config *Config) SetDBLogMode(db_logmode string) error {
|
||||||
|
if !allowedDBLogModes[db_logmode] {
|
||||||
|
return fmt.Errorf("unknown database log mode '%s'", db_logmode)
|
||||||
|
}
|
||||||
|
config.DBLogMode = db_logmode
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (config *Config) Read(input io.Reader) error {
|
func (config *Config) Read(input io.Reader) error {
|
||||||
return json.NewDecoder(input).Decode(config)
|
return json.NewDecoder(input).Decode(config)
|
||||||
}
|
}
|
||||||
|
|
6
config/search.go
Fichier normal
6
config/search.go
Fichier normal
|
@ -0,0 +1,6 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
type SearchConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultSearchConfig = SearchConfig{}
|
|
@ -10,6 +10,5 @@ var Trackers = []string{
|
||||||
"udp://explodie.org:6969",
|
"udp://explodie.org:6969",
|
||||||
"udp://tracker.opentrackr.org:1337",
|
"udp://tracker.opentrackr.org:1337",
|
||||||
"udp://tracker.internetwarriors.net:1337/announce",
|
"udp://tracker.internetwarriors.net:1337/announce",
|
||||||
"udp://eddie4.nl:6969/announce",
|
|
||||||
"http://mgtracker.org:6969/announce",
|
"http://mgtracker.org:6969/announce",
|
||||||
"http://tracker.baka-sub.cf/announce"}
|
"http://tracker.baka-sub.cf/announce"}
|
||||||
|
|
34
db/gorm.go
34
db/gorm.go
|
@ -9,16 +9,28 @@ import (
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
_ "github.com/jinzhu/gorm/dialects/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Print(v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// use the default gorm logger that prints to stdout
|
||||||
|
var DefaultLogger Logger = nil
|
||||||
|
|
||||||
var ORM *gorm.DB
|
var ORM *gorm.DB
|
||||||
|
|
||||||
|
var IsSqlite bool
|
||||||
|
|
||||||
// GormInit init gorm ORM.
|
// GormInit init gorm ORM.
|
||||||
func GormInit(conf *config.Config) (*gorm.DB, error) {
|
func GormInit(conf *config.Config, logger Logger) (*gorm.DB, error) {
|
||||||
|
|
||||||
db, openErr := gorm.Open(conf.DBType, conf.DBParams)
|
db, openErr := gorm.Open(conf.DBType, conf.DBParams)
|
||||||
if openErr != nil {
|
if openErr != nil {
|
||||||
log.CheckError(openErr)
|
log.CheckError(openErr)
|
||||||
return nil, openErr
|
return nil, openErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IsSqlite = conf.DBType == "sqlite"
|
||||||
|
|
||||||
connectionErr := db.DB().Ping()
|
connectionErr := db.DB().Ping()
|
||||||
if connectionErr != nil {
|
if connectionErr != nil {
|
||||||
log.CheckError(connectionErr)
|
log.CheckError(connectionErr)
|
||||||
|
@ -31,9 +43,29 @@ func GormInit(conf *config.Config) (*gorm.DB, error) {
|
||||||
db.LogMode(true)
|
db.LogMode(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch conf.DBLogMode {
|
||||||
|
case "detailed":
|
||||||
|
db.LogMode(true)
|
||||||
|
case "silent":
|
||||||
|
db.LogMode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if logger != nil {
|
||||||
|
db.SetLogger(logger)
|
||||||
|
}
|
||||||
|
|
||||||
db.AutoMigrate(&model.User{}, &model.UserFollows{}, &model.UserUploadsOld{})
|
db.AutoMigrate(&model.User{}, &model.UserFollows{}, &model.UserUploadsOld{})
|
||||||
|
if db.Error != nil {
|
||||||
|
return db, db.Error
|
||||||
|
}
|
||||||
db.AutoMigrate(&model.Torrent{}, &model.TorrentReport{})
|
db.AutoMigrate(&model.Torrent{}, &model.TorrentReport{})
|
||||||
|
if db.Error != nil {
|
||||||
|
return db, db.Error
|
||||||
|
}
|
||||||
db.AutoMigrate(&model.Comment{}, &model.OldComment{})
|
db.AutoMigrate(&model.Comment{}, &model.OldComment{})
|
||||||
|
if db.Error != nil {
|
||||||
|
return db, db.Error
|
||||||
|
}
|
||||||
|
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,81 @@
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/azhao12345/gorm"
|
||||||
"github.com/ewhal/nyaa/config"
|
"github.com/ewhal/nyaa/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGormInit(t *testing.T) {
|
type errorLogger struct {
|
||||||
|
t *testing.T
|
||||||
|
}
|
||||||
|
|
||||||
|
func (logger *errorLogger) Print(values ...interface{}) {
|
||||||
|
if len(values) > 1 {
|
||||||
|
message := gorm.LogFormatter(values...)
|
||||||
|
level := values[0]
|
||||||
|
if level == "log" {
|
||||||
|
logger.t.Error(message...)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(message...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormInitSqlite(t *testing.T) {
|
||||||
conf := config.New()
|
conf := config.New()
|
||||||
conf.DBType = "sqlite3"
|
conf.DBType = "sqlite3"
|
||||||
conf.DBParams = ":memory:?cache=shared&mode=memory"
|
conf.DBParams = ":memory:?cache=shared&mode=memory"
|
||||||
|
conf.DBLogMode = "detailed"
|
||||||
|
|
||||||
db, err := GormInit(conf)
|
db, err := GormInit(conf, &errorLogger{t})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to initialize database: %v", err)
|
t.Errorf("failed to initialize database: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to close database: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This test requires a running postgres instance. To run it in CI build add these settings in the .travis.yml
|
||||||
|
// services:
|
||||||
|
// - postgresql
|
||||||
|
// before_script:
|
||||||
|
// - psql -c "CREATE DATABASE nyaapantsu;" -U postgres
|
||||||
|
// - psql -c "CREATE USER nyaapantsu WITH PASSWORD 'nyaapantsu';" -U postgres
|
||||||
|
//
|
||||||
|
// Then enable the test by setting this variable to "true" via ldflags:
|
||||||
|
// go test ./... -v -ldflags="-X github.com/ewhal/nyaa/db.testPostgres=true"
|
||||||
|
var testPostgres = "false"
|
||||||
|
|
||||||
|
func TestGormInitPostgres(t *testing.T) {
|
||||||
|
if testPostgres != "true" {
|
||||||
|
t.Skip("skip", testPostgres)
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := config.New()
|
||||||
|
conf.DBType = "postgres"
|
||||||
|
conf.DBParams = "host=localhost user=nyaapantsu dbname=nyaapantsu sslmode=disable password=nyaapantsu"
|
||||||
|
conf.DBLogMode = "detailed"
|
||||||
|
|
||||||
|
db, err := GormInit(conf, &errorLogger{t})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to initialize database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if db == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = db.Close()
|
err = db.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to close database: %v", err)
|
t.Errorf("failed to close database: %v", err)
|
||||||
|
|
15
main.go
15
main.go
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/ewhal/nyaa/router"
|
"github.com/ewhal/nyaa/router"
|
||||||
"github.com/ewhal/nyaa/service/scraper"
|
"github.com/ewhal/nyaa/service/scraper"
|
||||||
"github.com/ewhal/nyaa/util/log"
|
"github.com/ewhal/nyaa/util/log"
|
||||||
|
"github.com/ewhal/nyaa/util/search"
|
||||||
"github.com/ewhal/nyaa/util/signals"
|
"github.com/ewhal/nyaa/util/signals"
|
||||||
"github.com/nicksnyder/go-i18n/i18n"
|
"github.com/nicksnyder/go-i18n/i18n"
|
||||||
)
|
)
|
||||||
|
@ -84,6 +85,7 @@ func RunScraper(conf *config.Config) {
|
||||||
signals.RegisterCloser(scraper)
|
signals.RegisterCloser(scraper)
|
||||||
// run udp scraper worker
|
// run udp scraper worker
|
||||||
for workers > 0 {
|
for workers > 0 {
|
||||||
|
log.Infof("starting up worker %d", workers)
|
||||||
go scraper.RunWorker(pc)
|
go scraper.RunWorker(pc)
|
||||||
workers--
|
workers--
|
||||||
}
|
}
|
||||||
|
@ -97,7 +99,8 @@ func main() {
|
||||||
processFlags := conf.BindFlags()
|
processFlags := conf.BindFlags()
|
||||||
defaults := flag.Bool("print-defaults", false, "print the default configuration file on stdout")
|
defaults := flag.Bool("print-defaults", false, "print the default configuration file on stdout")
|
||||||
mode := flag.String("mode", "webapp", "which mode to run daemon in, either webapp or scraper")
|
mode := flag.String("mode", "webapp", "which mode to run daemon in, either webapp or scraper")
|
||||||
flag.Float64Var(&cache.Size, "c", cache.Size, "size of the search cache in MB")
|
flag.Float64Var(&conf.Cache.Size, "c", config.DefaultCacheSize, "size of the search cache in MB")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if *defaults {
|
if *defaults {
|
||||||
stdout := bufio.NewWriter(os.Stdout)
|
stdout := bufio.NewWriter(os.Stdout)
|
||||||
|
@ -115,11 +118,19 @@ func main() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.CheckError(err)
|
log.CheckError(err)
|
||||||
}
|
}
|
||||||
db.ORM, err = db.GormInit(conf)
|
db.ORM, err = db.GormInit(conf, db.DefaultLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err.Error())
|
log.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
initI18N()
|
initI18N()
|
||||||
|
err = cache.Configure(&conf.Cache)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
err = search.Configure(&conf.Search)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
go signals.Handle()
|
go signals.Handle()
|
||||||
if len(config.TorrentFileStorage) > 0 {
|
if len(config.TorrentFileStorage) > 0 {
|
||||||
err := os.MkdirAll(config.TorrentFileStorage, 0700)
|
err := os.MkdirAll(config.TorrentFileStorage, 0700)
|
||||||
|
|
|
@ -12,8 +12,9 @@ type User struct {
|
||||||
Status int `gorm:"column:status"`
|
Status int `gorm:"column:status"`
|
||||||
CreatedAt time.Time `gorm:"column:created_at"`
|
CreatedAt time.Time `gorm:"column:created_at"`
|
||||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||||
Token string `gorm:"column:api_token"`
|
// Currently unused (auth is stateless now)
|
||||||
TokenExpiration time.Time `gorm:"column:api_token_expiry"`
|
/*Token string `gorm:"column:api_token"`
|
||||||
|
TokenExpiration time.Time `gorm:"column:api_token_expiry"`*/
|
||||||
Language string `gorm:"column:language"`
|
Language string `gorm:"column:language"`
|
||||||
|
|
||||||
// TODO: move this to PublicUser
|
// TODO: move this to PublicUser
|
||||||
|
@ -42,7 +43,7 @@ func (u User) Size() (s int) {
|
||||||
4*3 + //time.Time
|
4*3 + //time.Time
|
||||||
3*2 + // arrays
|
3*2 + // arrays
|
||||||
// string arrays
|
// string arrays
|
||||||
len(u.Username) + len(u.Password) + len(u.Email) + len(u.Token) + len(u.MD5) + len(u.Language)
|
len(u.Username) + len(u.Password) + len(u.Email) + len(u.MD5) + len(u.Language)
|
||||||
s *= 8
|
s *= 8
|
||||||
|
|
||||||
// Ignoring foreign key users. Fuck them.
|
// Ignoring foreign key users. Fuck them.
|
||||||
|
|
|
@ -143,4 +143,9 @@ a:hover {
|
||||||
}
|
}
|
||||||
.modal-content .close {
|
.modal-content .close {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
background: #29363d;
|
||||||
|
color: #cf9fff;
|
||||||
|
}
|
||||||
|
|
|
@ -383,3 +383,8 @@ footer {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
width: auto; /* Undo bootstrap's fixed width */
|
width: auto; /* Undo bootstrap's fixed width */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-error {
|
||||||
|
background: white;
|
||||||
|
color: #cf9fff;
|
||||||
|
}
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
// Night mode
|
|
||||||
var night = localStorage.getItem("night");
|
var night = localStorage.getItem("night");
|
||||||
if (night == "true") {
|
|
||||||
$("head").append('<link id="style-dark" rel="stylesheet" type="text/css" href="/css/style-night.css">');
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleNightMode() {
|
function toggleNightMode() {
|
||||||
var night = localStorage.getItem("night");
|
var night = localStorage.getItem("night");
|
||||||
if(night == "true") {
|
if(night == "true") {
|
||||||
$("#style-dark")[0].remove()
|
document.getElementById("style-dark").remove()
|
||||||
} else {
|
} else {
|
||||||
$("head").append('<link id="style-dark" rel="stylesheet" type="text/css" href="/css/style-night.css">');
|
document.getElementsByTagName("head")[0].append(darkStyleLink);
|
||||||
}
|
}
|
||||||
localStorage.setItem("night", (night == "true") ? "false" : "true");
|
localStorage.setItem("night", (night == "true") ? "false" : "true");
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,8 @@ func HomeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Max: uint(maxPerPage),
|
Max: uint(maxPerPage),
|
||||||
Page: pagenum,
|
Page: pagenum,
|
||||||
}
|
}
|
||||||
torrents, nbTorrents, err := cache.Get(search, func() ([]model.Torrent, int, error) {
|
|
||||||
|
torrents, nbTorrents, err := cache.Impl.Get(search, func() ([]model.Torrent, int, error) {
|
||||||
torrents, nbTorrents, err := torrentService.GetAllTorrents(maxPerPage, maxPerPage*(pagenum-1))
|
torrents, nbTorrents, err := torrentService.GetAllTorrents(maxPerPage, maxPerPage*(pagenum-1))
|
||||||
if !log.CheckError(err) {
|
if !log.CheckError(err) {
|
||||||
util.SendError(w, err, 400)
|
util.SendError(w, err, 400)
|
||||||
|
|
|
@ -3,9 +3,7 @@ package router
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/ewhal/nyaa/db"
|
"github.com/ewhal/nyaa/db"
|
||||||
|
@ -23,23 +21,6 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
panelTorrentList = template.Must(template.New("torrentlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/torrentlist.html")))
|
|
||||||
panelTorrentList = template.Must(panelTorrentList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
panelUserList = template.Must(template.New("userlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/userlist.html")))
|
|
||||||
panelUserList = template.Must(panelUserList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
panelCommentList = template.Must(template.New("commentlist").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/commentlist.html")))
|
|
||||||
panelCommentList = template.Must(panelCommentList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
panelIndex = template.Must(template.New("indexPanel").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/panelindex.html")))
|
|
||||||
panelIndex = template.Must(panelIndex.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
panelTorrentEd = template.Must(template.New("torrent_ed").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/paneltorrentedit.html")))
|
|
||||||
panelTorrentEd = template.Must(panelTorrentEd.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
panelTorrentReportList = template.Must(template.New("torrent_report").Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "admin_index.html"), filepath.Join(TemplateDir, "admin/torrent_report.html")))
|
|
||||||
panelTorrentReportList = template.Must(panelTorrentReportList.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
|
||||||
}
|
|
||||||
|
|
||||||
func IndexModPanel(w http.ResponseWriter, r *http.Request) {
|
func IndexModPanel(w http.ResponseWriter, r *http.Request) {
|
||||||
currentUser := GetUser(r)
|
currentUser := GetUser(r)
|
||||||
if userPermission.HasAdmin(currentUser) {
|
if userPermission.HasAdmin(currentUser) {
|
||||||
|
@ -217,19 +198,19 @@ func TorrentPostEditModPanel(w http.ResponseWriter, r *http.Request) {
|
||||||
err := form.NewErrors()
|
err := form.NewErrors()
|
||||||
infos := form.NewInfos()
|
infos := form.NewInfos()
|
||||||
torrent, _ := torrentService.GetTorrentById(id)
|
torrent, _ := torrentService.GetTorrentById(id)
|
||||||
if (torrent.ID > 0) {
|
if torrent.ID > 0 {
|
||||||
errUp := uploadForm.ExtractEditInfo(r)
|
errUp := uploadForm.ExtractEditInfo(r)
|
||||||
if errUp != nil {
|
if errUp != nil {
|
||||||
err["errors"] = append(err["errors"], "Failed to update torrent!")
|
err["errors"] = append(err["errors"], "Failed to update torrent!")
|
||||||
}
|
}
|
||||||
if (len(err) == 0) {
|
if len(err) == 0 {
|
||||||
// update some (but not all!) values
|
// update some (but not all!) values
|
||||||
torrent.Name = uploadForm.Name
|
torrent.Name = uploadForm.Name
|
||||||
torrent.Category = uploadForm.CategoryID
|
torrent.Category = uploadForm.CategoryID
|
||||||
torrent.SubCategory = uploadForm.SubCategoryID
|
torrent.SubCategory = uploadForm.SubCategoryID
|
||||||
torrent.Status = uploadForm.Status
|
torrent.Status = uploadForm.Status
|
||||||
torrent.Description = uploadForm.Description
|
torrent.Description = uploadForm.Description
|
||||||
torrent.Uploader = nil // GORM will create a new user otherwise (wtf?!)
|
torrent.Uploader = nil // GORM will create a new user otherwise (wtf?!)
|
||||||
db.ORM.Save(&torrent)
|
db.ORM.Save(&torrent)
|
||||||
infos["infos"] = append(infos["infos"], "Torrent details updated.")
|
infos["infos"] = append(infos["infos"], "Torrent details updated.")
|
||||||
}
|
}
|
||||||
|
|
148
router/router.go
148
router/router.go
|
@ -4,7 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/ewhal/nyaa/service/captcha"
|
"github.com/ewhal/nyaa/service/captcha"
|
||||||
"github.com/gorilla/handlers"
|
// "github.com/gorilla/handlers"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,102 +15,102 @@ func init() {
|
||||||
cssHandler := http.FileServer(http.Dir("./public/css/"))
|
cssHandler := http.FileServer(http.Dir("./public/css/"))
|
||||||
jsHandler := http.FileServer(http.Dir("./public/js/"))
|
jsHandler := http.FileServer(http.Dir("./public/js/"))
|
||||||
imgHandler := http.FileServer(http.Dir("./public/img/"))
|
imgHandler := http.FileServer(http.Dir("./public/img/"))
|
||||||
|
gzipHomeHandler := http.HandlerFunc(HomeHandler)
|
||||||
|
gzipAPIHandler := http.HandlerFunc(ApiHandler)
|
||||||
|
gzipAPIViewHandler := http.HandlerFunc(ApiViewHandler)
|
||||||
|
gzipViewHandler := http.HandlerFunc(ViewHandler)
|
||||||
|
gzipUserProfileHandler := http.HandlerFunc(UserProfileHandler)
|
||||||
|
gzipUserDetailsHandler := http.HandlerFunc(UserDetailsHandler)
|
||||||
|
gzipUserProfileFormHandler := http.HandlerFunc(UserProfileFormHandler)
|
||||||
|
/*
|
||||||
// Enable GZIP compression for all handlers except imgHandler and captcha
|
// Enable GZIP compression for all handlers except imgHandler and captcha
|
||||||
gzipCSSHandler := handlers.CompressHandler(cssHandler)
|
gzipCSSHandler := cssHandler)
|
||||||
gzipJSHandler := handlers.CompressHandler(jsHandler)
|
gzipJSHandler:= jsHandler)
|
||||||
gzipHomeHandler := handlers.CompressHandler(http.HandlerFunc(HomeHandler))
|
gzipSearchHandler:= http.HandlerFunc(SearchHandler)
|
||||||
gzipSearchHandler := handlers.CompressHandler(http.HandlerFunc(SearchHandler))
|
gzipAPIUploadHandler := http.HandlerFunc(ApiUploadHandler)
|
||||||
gzipAPIHandler := handlers.CompressHandler(http.HandlerFunc(ApiHandler))
|
gzipAPIUpdateHandler := http.HandlerFunc(ApiUpdateHandler)
|
||||||
gzipAPIViewHandler := handlers.CompressHandler(http.HandlerFunc(ApiViewHandler))
|
gzipFaqHandler := http.HandlerFunc(FaqHandler)
|
||||||
gzipAPIUploadHandler := handlers.CompressHandler(http.HandlerFunc(ApiUploadHandler))
|
gzipRSSHandler := http.HandlerFunc(RSSHandler)
|
||||||
gzipAPIUpdateHandler := handlers.CompressHandler(http.HandlerFunc(ApiUpdateHandler))
|
gzipUploadHandler := http.HandlerFunc(UploadHandler)
|
||||||
gzipFaqHandler := handlers.CompressHandler(http.HandlerFunc(FaqHandler))
|
gzipUserRegisterFormHandler := http.HandlerFunc(UserRegisterFormHandler)
|
||||||
gzipRSSHandler := handlers.CompressHandler(http.HandlerFunc(RSSHandler))
|
gzipUserLoginFormHandler := http.HandlerFunc(UserLoginFormHandler)
|
||||||
gzipViewHandler := handlers.CompressHandler(http.HandlerFunc(ViewHandler))
|
gzipUserVerifyEmailHandler := http.HandlerFunc(UserVerifyEmailHandler)
|
||||||
gzipUploadHandler := handlers.CompressHandler(http.HandlerFunc(UploadHandler))
|
gzipUserRegisterPostHandler := http.HandlerFunc(UserRegisterPostHandler)
|
||||||
gzipUserRegisterFormHandler := handlers.CompressHandler(http.HandlerFunc(UserRegisterFormHandler))
|
gzipUserLoginPostHandler := http.HandlerFunc(UserLoginPostHandler)
|
||||||
gzipUserLoginFormHandler := handlers.CompressHandler(http.HandlerFunc(UserLoginFormHandler))
|
gzipUserLogoutHandler := http.HandlerFunc(UserLogoutHandler)
|
||||||
gzipUserVerifyEmailHandler := handlers.CompressHandler(http.HandlerFunc(UserVerifyEmailHandler))
|
gzipUserFollowHandler := http.HandlerFunc(UserFollowHandler)
|
||||||
gzipUserRegisterPostHandler := handlers.CompressHandler(http.HandlerFunc(UserRegisterPostHandler))
|
|
||||||
gzipUserLoginPostHandler := handlers.CompressHandler(http.HandlerFunc(UserLoginPostHandler))
|
|
||||||
gzipUserLogoutHandler := handlers.CompressHandler(http.HandlerFunc(UserLogoutHandler))
|
|
||||||
gzipUserProfileHandler := handlers.CompressHandler(http.HandlerFunc(UserProfileHandler))
|
|
||||||
gzipUserFollowHandler := handlers.CompressHandler(http.HandlerFunc(UserFollowHandler))
|
|
||||||
gzipUserDetailsHandler := handlers.CompressHandler(http.HandlerFunc(UserDetailsHandler))
|
|
||||||
gzipUserProfileFormHandler := handlers.CompressHandler(http.HandlerFunc(UserProfileFormHandler))
|
|
||||||
|
|
||||||
gzipIndexModPanel := handlers.CompressHandler(http.HandlerFunc(IndexModPanel))
|
gzipIndexModPanel := http.HandlerFunc(IndexModPanel)
|
||||||
gzipTorrentsListPanel := handlers.CompressHandler(http.HandlerFunc(TorrentsListPanel))
|
gzipTorrentsListPanel := http.HandlerFunc(TorrentsListPanel)
|
||||||
gzipTorrentReportListPanel := handlers.CompressHandler(http.HandlerFunc(TorrentReportListPanel))
|
gzipTorrentReportListPanel := http.HandlerFunc(TorrentReportListPanel)
|
||||||
gzipUsersListPanel := handlers.CompressHandler(http.HandlerFunc(UsersListPanel))
|
gzipUsersListPanel := http.HandlerFunc(UsersListPanel)
|
||||||
gzipCommentsListPanel := handlers.CompressHandler(http.HandlerFunc(CommentsListPanel))
|
gzipCommentsListPanel := http.HandlerFunc(CommentsListPanel)
|
||||||
gzipTorrentEditModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentEditModPanel))
|
gzipTorrentEditModPanel := http.HandlerFunc(TorrentEditModPanel)
|
||||||
gzipTorrentPostEditModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentPostEditModPanel))
|
gzipTorrentPostEditModPanel := http.HandlerFunc(TorrentPostEditModPanel)
|
||||||
gzipCommentDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(CommentDeleteModPanel))
|
gzipCommentDeleteModPanel := http.HandlerFunc(CommentDeleteModPanel)
|
||||||
gzipTorrentDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentDeleteModPanel))
|
gzipTorrentDeleteModPanel := http.HandlerFunc(TorrentDeleteModPanel)
|
||||||
gzipTorrentReportDeleteModPanel := handlers.CompressHandler(http.HandlerFunc(TorrentReportDeleteModPanel))
|
gzipTorrentReportDeleteModPanel := http.HandlerFunc(TorrentReportDeleteModPanel)*/
|
||||||
|
|
||||||
//gzipTorrentReportCreateHandler := handlers.CompressHandler(http.HandlerFunc(CreateTorrentReportHandler))
|
//gzipTorrentReportCreateHandler := http.HandlerFunc(CreateTorrentReportHandler)
|
||||||
//gzipTorrentReportDeleteHandler := handlers.CompressHandler(http.HandlerFunc(DeleteTorrentReportHandler))
|
//gzipTorrentReportDeleteHandler := http.HandlerFunc(DeleteTorrentReportHandler)
|
||||||
//gzipTorrentDeleteHandler := handlers.CompressHandler(http.HandlerFunc(DeleteTorrentHandler))
|
//gzipTorrentDeleteHandler := http.HandlerFunc(DeleteTorrentHandler)
|
||||||
|
|
||||||
Router = mux.NewRouter()
|
Router = mux.NewRouter()
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
http.Handle("/css/", http.StripPrefix("/css/", wrapHandler(gzipCSSHandler)))
|
http.Handle("/css/", http.StripPrefix("/css/", cssHandler))
|
||||||
http.Handle("/js/", http.StripPrefix("/js/", wrapHandler(gzipJSHandler)))
|
http.Handle("/js/", http.StripPrefix("/js/", jsHandler))
|
||||||
http.Handle("/img/", http.StripPrefix("/img/", wrapHandler(imgHandler)))
|
http.Handle("/img/", http.StripPrefix("/img/", imgHandler))
|
||||||
Router.Handle("/", gzipHomeHandler).Name("home")
|
Router.Handle("/", wrapHandler(gzipHomeHandler)).Name("home")
|
||||||
Router.Handle("/page/{page:[0-9]+}", wrapHandler(gzipHomeHandler)).Name("home_page")
|
Router.Handle("/page/{page:[0-9]+}", wrapHandler(gzipHomeHandler)).Name("home_page")
|
||||||
Router.Handle("/search", gzipSearchHandler).Name("search")
|
Router.HandleFunc("/search", SearchHandler).Name("search")
|
||||||
Router.Handle("/search/{page}", gzipSearchHandler).Name("search_page")
|
Router.HandleFunc("/search/{page}", SearchHandler).Name("search_page")
|
||||||
Router.Handle("/api", gzipAPIHandler).Methods("GET")
|
Router.Handle("/api", wrapHandler(gzipAPIHandler)).Methods("GET")
|
||||||
Router.Handle("/api/{page:[0-9]*}", wrapHandler(gzipAPIHandler)).Methods("GET")
|
Router.Handle("/api/{page:[0-9]*}", wrapHandler(gzipAPIHandler)).Methods("GET")
|
||||||
Router.Handle("/api/view/{id}", wrapHandler(gzipAPIViewHandler)).Methods("GET")
|
Router.Handle("/api/view/{id}", wrapHandler(gzipAPIViewHandler)).Methods("GET")
|
||||||
Router.Handle("/api/upload", gzipAPIUploadHandler).Methods("POST")
|
Router.HandleFunc("/api/upload", ApiUploadHandler).Methods("POST")
|
||||||
Router.Handle("/api/update", gzipAPIUpdateHandler).Methods("PUT")
|
Router.HandleFunc("/api/update", ApiUpdateHandler).Methods("PUT")
|
||||||
Router.Handle("/faq", gzipFaqHandler).Name("faq")
|
Router.HandleFunc("/faq", FaqHandler).Name("faq")
|
||||||
Router.Handle("/feed", gzipRSSHandler).Name("feed")
|
Router.HandleFunc("/feed", RSSHandler).Name("feed")
|
||||||
Router.Handle("/view/{id}", wrapHandler(gzipViewHandler)).Methods("GET").Name("view_torrent")
|
Router.Handle("/view/{id}", wrapHandler(gzipViewHandler)).Methods("GET").Name("view_torrent")
|
||||||
Router.HandleFunc("/view/{id}", PostCommentHandler).Methods("POST").Name("post_comment")
|
Router.HandleFunc("/view/{id}", PostCommentHandler).Methods("POST").Name("post_comment")
|
||||||
Router.Handle("/upload", gzipUploadHandler).Name("upload")
|
Router.HandleFunc("/upload", UploadHandler).Name("upload")
|
||||||
Router.Handle("/user/register", gzipUserRegisterFormHandler).Name("user_register").Methods("GET")
|
Router.HandleFunc("/user/register", UserRegisterFormHandler).Name("user_register").Methods("GET")
|
||||||
Router.Handle("/user/login", gzipUserLoginFormHandler).Name("user_login").Methods("GET")
|
Router.HandleFunc("/user/login", UserLoginFormHandler).Name("user_login").Methods("GET")
|
||||||
Router.Handle("/verify/email/{token}", gzipUserVerifyEmailHandler).Name("user_verify").Methods("GET")
|
Router.HandleFunc("/verify/email/{token}", UserVerifyEmailHandler).Name("user_verify").Methods("GET")
|
||||||
Router.Handle("/user/register", gzipUserRegisterPostHandler).Name("user_register").Methods("POST")
|
Router.HandleFunc("/user/register", UserRegisterPostHandler).Name("user_register").Methods("POST")
|
||||||
Router.Handle("/user/login", gzipUserLoginPostHandler).Name("user_login").Methods("POST")
|
Router.HandleFunc("/user/login", UserLoginPostHandler).Name("user_login").Methods("POST")
|
||||||
Router.Handle("/user/logout", gzipUserLogoutHandler).Name("user_logout")
|
Router.HandleFunc("/user/logout", UserLogoutHandler).Name("user_logout")
|
||||||
Router.Handle("/user/{id}/{username}", wrapHandler(gzipUserProfileHandler)).Name("user_profile").Methods("GET")
|
Router.Handle("/user/{id}/{username}", wrapHandler(gzipUserProfileHandler)).Name("user_profile").Methods("GET")
|
||||||
Router.Handle("/user/{id}/{username}/follow", gzipUserFollowHandler).Name("user_follow").Methods("GET")
|
Router.HandleFunc("/user/{id}/{username}/follow", UserFollowHandler).Name("user_follow").Methods("GET")
|
||||||
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserDetailsHandler)).Name("user_profile_details").Methods("GET")
|
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserDetailsHandler)).Name("user_profile_details").Methods("GET")
|
||||||
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserProfileFormHandler)).Name("user_profile_edit").Methods("POST")
|
Router.Handle("/user/{id}/{username}/edit", wrapHandler(gzipUserProfileFormHandler)).Name("user_profile_edit").Methods("POST")
|
||||||
|
|
||||||
Router.Handle("/mod", gzipIndexModPanel).Name("mod_index")
|
Router.HandleFunc("/mod", IndexModPanel).Name("mod_index")
|
||||||
Router.Handle("/mod/torrents", gzipTorrentsListPanel).Name("mod_tlist")
|
Router.HandleFunc("/mod/torrents", TorrentsListPanel).Name("mod_tlist")
|
||||||
Router.Handle("/mod/torrents/{page}", gzipTorrentsListPanel).Name("mod_tlist_page")
|
Router.HandleFunc("/mod/torrents/{page}", TorrentsListPanel).Name("mod_tlist_page")
|
||||||
Router.Handle("/mod/reports", gzipTorrentReportListPanel).Name("mod_trlist")
|
Router.HandleFunc("/mod/reports", TorrentReportListPanel).Name("mod_trlist")
|
||||||
Router.Handle("/mod/reports/{page}", gzipTorrentReportListPanel).Name("mod_trlist_page")
|
Router.HandleFunc("/mod/reports/{page}", TorrentReportListPanel).Name("mod_trlist_page")
|
||||||
Router.Handle("/mod/users", gzipUsersListPanel).Name("mod_ulist")
|
Router.HandleFunc("/mod/users", UsersListPanel).Name("mod_ulist")
|
||||||
Router.Handle("/mod/users/{page}", gzipUsersListPanel).Name("mod_ulist_page")
|
Router.HandleFunc("/mod/users/{page}", UsersListPanel).Name("mod_ulist_page")
|
||||||
Router.Handle("/mod/comments", gzipCommentsListPanel).Name("mod_clist")
|
Router.HandleFunc("/mod/comments", CommentsListPanel).Name("mod_clist")
|
||||||
Router.Handle("/mod/comments/{page}", gzipCommentsListPanel).Name("mod_clist_page")
|
Router.HandleFunc("/mod/comments/{page}", CommentsListPanel).Name("mod_clist_page")
|
||||||
Router.Handle("/mod/comment", gzipCommentsListPanel).Name("mod_cedit") // TODO
|
Router.HandleFunc("/mod/comment", CommentsListPanel).Name("mod_cedit") // TODO
|
||||||
Router.Handle("/mod/torrent/", gzipTorrentEditModPanel).Name("mod_tedit").Methods("GET")
|
Router.HandleFunc("/mod/torrent/", TorrentEditModPanel).Name("mod_tedit").Methods("GET")
|
||||||
Router.Handle("/mod/torrent/", gzipTorrentPostEditModPanel).Name("mod_ptedit").Methods("POST")
|
Router.HandleFunc("/mod/torrent/", TorrentPostEditModPanel).Name("mod_ptedit").Methods("POST")
|
||||||
Router.Handle("/mod/torrent/delete", gzipTorrentDeleteModPanel).Name("mod_tdelete")
|
Router.HandleFunc("/mod/torrent/delete", TorrentDeleteModPanel).Name("mod_tdelete")
|
||||||
Router.Handle("/mod/report/delete", gzipTorrentReportDeleteModPanel).Name("mod_trdelete")
|
Router.HandleFunc("/mod/report/delete", TorrentReportDeleteModPanel).Name("mod_trdelete")
|
||||||
Router.Handle("/mod/comment/delete", gzipCommentDeleteModPanel).Name("mod_cdelete")
|
Router.HandleFunc("/mod/comment/delete", CommentDeleteModPanel).Name("mod_cdelete")
|
||||||
|
|
||||||
//reporting a torrent
|
//reporting a torrent
|
||||||
Router.HandleFunc("/report/{id}", ReportTorrentHandler).Methods("POST").Name("post_comment")
|
Router.HandleFunc("/report/{id}", ReportTorrentHandler).Methods("POST").Name("post_comment")
|
||||||
|
|
||||||
Router.PathPrefix("/captcha").Methods("GET").HandlerFunc(captcha.ServeFiles)
|
Router.PathPrefix("/captcha").Methods("GET").HandlerFunc(captcha.ServeFiles)
|
||||||
|
|
||||||
//Router.Handle("/report/create", gzipTorrentReportCreateHandler).Name("torrent_report_create").Methods("POST")
|
//Router.HandleFunc("/report/create", gzipTorrentReportCreateHandler).Name("torrent_report_create").Methods("POST")
|
||||||
// TODO Allow only moderators to access /moderation/*
|
// TODO Allow only moderators to access /moderation/*
|
||||||
//Router.Handle("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
|
//Router.HandleFunc("/moderation/report/delete", gzipTorrentReportDeleteHandler).Name("torrent_report_delete").Methods("POST")
|
||||||
//Router.Handle("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
|
//Router.HandleFunc("/moderation/torrent/delete", gzipTorrentDeleteHandler).Name("torrent_delete").Methods("POST")
|
||||||
|
|
||||||
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
|
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,22 +26,21 @@ func RSSHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
Link: &feeds.Link{Href: "https://" + config.WebAddress + "/"},
|
Link: &feeds.Link{Href: "https://" + config.WebAddress + "/"},
|
||||||
Created: createdAsTime,
|
Created: createdAsTime,
|
||||||
}
|
}
|
||||||
feed.Items = []*feeds.Item{}
|
|
||||||
feed.Items = make([]*feeds.Item, len(torrents))
|
feed.Items = make([]*feeds.Item, len(torrents))
|
||||||
|
|
||||||
for i := range torrents {
|
for i, torrent := range torrents {
|
||||||
torrentJSON := torrents[i].ToJSON()
|
torrentJSON := torrent.ToJSON()
|
||||||
feed.Items[i] = &feeds.Item{
|
feed.Items[i] = &feeds.Item{
|
||||||
// need a torrent view first
|
|
||||||
Id: "https://" + config.WebAddress + "/view/" + strconv.FormatUint(uint64(torrents[i].ID), 10),
|
Id: "https://" + config.WebAddress + "/view/" + strconv.FormatUint(uint64(torrents[i].ID), 10),
|
||||||
Title: torrents[i].Name,
|
Title: torrent.Name,
|
||||||
Link: &feeds.Link{Href: string(torrentJSON.Magnet)},
|
Link: &feeds.Link{Href: string(torrentJSON.Magnet)},
|
||||||
Description: "",
|
Description: string(torrentJSON.Description),
|
||||||
Created: torrents[0].Date,
|
Created: torrent.Date,
|
||||||
Updated: torrents[0].Date,
|
Updated: torrent.Date,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// allow cross domain AJAX requests
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
rss, rssErr := feed.ToRss()
|
rss, rssErr := feed.ToRss()
|
||||||
if rssErr != nil {
|
if rssErr != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
|
@ -9,15 +9,18 @@ var TemplateDir = "templates"
|
||||||
|
|
||||||
var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate *template.Template
|
var homeTemplate, searchTemplate, faqTemplate, uploadTemplate, viewTemplate, viewRegisterTemplate, viewLoginTemplate, viewRegisterSuccessTemplate, viewVerifySuccessTemplate, viewProfileTemplate, viewProfileEditTemplate, viewUserDeleteTemplate, notFoundTemplate *template.Template
|
||||||
|
|
||||||
|
var panelIndex, panelTorrentList, panelUserList, panelCommentList, panelTorrentEd, panelTorrentReportList *template.Template
|
||||||
|
|
||||||
type templateLoader struct {
|
type templateLoader struct {
|
||||||
templ **template.Template
|
templ **template.Template
|
||||||
file string
|
file string
|
||||||
name string
|
indexFile string
|
||||||
|
name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReloadTemplates reloads templates on runtime
|
// ReloadTemplates reloads templates on runtime
|
||||||
func ReloadTemplates() {
|
func ReloadTemplates() {
|
||||||
templs := []templateLoader{
|
pubTempls := []templateLoader{
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &homeTemplate,
|
templ: &homeTemplate,
|
||||||
name: "home",
|
name: "home",
|
||||||
|
@ -46,37 +49,37 @@ func ReloadTemplates() {
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewRegisterTemplate,
|
templ: &viewRegisterTemplate,
|
||||||
name: "user_register",
|
name: "user_register",
|
||||||
file: "user/register.html",
|
file: filepath.Join("user", "register.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewRegisterSuccessTemplate,
|
templ: &viewRegisterSuccessTemplate,
|
||||||
name: "user_register_success",
|
name: "user_register_success",
|
||||||
file: "user/signup_success.html",
|
file: filepath.Join("user", "signup_success.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewVerifySuccessTemplate,
|
templ: &viewVerifySuccessTemplate,
|
||||||
name: "user_verify_success",
|
name: "user_verify_success",
|
||||||
file: "user/verify_success.html",
|
file: filepath.Join("user", "verify_success.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewLoginTemplate,
|
templ: &viewLoginTemplate,
|
||||||
name: "user_login",
|
name: "user_login",
|
||||||
file: "user/login.html",
|
file: filepath.Join("user", "login.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewProfileTemplate,
|
templ: &viewProfileTemplate,
|
||||||
name: "user_profile",
|
name: "user_profile",
|
||||||
file: "user/profile.html",
|
file: filepath.Join("user", "profile.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewProfileEditTemplate,
|
templ: &viewProfileEditTemplate,
|
||||||
name: "user_profile",
|
name: "user_profile",
|
||||||
file: "user/profile_edit.html",
|
file: filepath.Join("user", "profile_edit.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: &viewUserDeleteTemplate,
|
templ: &viewUserDeleteTemplate,
|
||||||
name: "user_delete",
|
name: "user_delete",
|
||||||
file: "user/delete_success.html",
|
file: filepath.Join("user", "delete_success.html"),
|
||||||
},
|
},
|
||||||
templateLoader{
|
templateLoader{
|
||||||
templ: ¬FoundTemplate,
|
templ: ¬FoundTemplate,
|
||||||
|
@ -84,10 +87,56 @@ func ReloadTemplates() {
|
||||||
file: "404.html",
|
file: "404.html",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, templ := range templs {
|
for idx := range pubTempls {
|
||||||
t := template.Must(template.New(templ.name).Funcs(FuncMap).ParseFiles(filepath.Join(TemplateDir, "index.html"), filepath.Join(TemplateDir, templ.file)))
|
pubTempls[idx].indexFile = filepath.Join(TemplateDir, "index.html")
|
||||||
t = template.Must(t.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
}
|
||||||
|
|
||||||
|
modTempls := []templateLoader{
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelTorrentList,
|
||||||
|
name: "torrentlist",
|
||||||
|
file: filepath.Join("admin", "torrentlist.html"),
|
||||||
|
},
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelUserList,
|
||||||
|
name: "userlist",
|
||||||
|
file: filepath.Join("admin", "userlist.html"),
|
||||||
|
},
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelCommentList,
|
||||||
|
name: "commentlist",
|
||||||
|
file: filepath.Join("admin", "commentlist.html"),
|
||||||
|
},
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelIndex,
|
||||||
|
name: "indexPanel",
|
||||||
|
file: filepath.Join("admin", "panelindex.html"),
|
||||||
|
},
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelTorrentEd,
|
||||||
|
name: "torrent_ed",
|
||||||
|
file: filepath.Join("admin", "paneltorrentedit.html"),
|
||||||
|
},
|
||||||
|
templateLoader{
|
||||||
|
templ: &panelTorrentReportList,
|
||||||
|
name: "torrent_report",
|
||||||
|
file: filepath.Join("admin", "torrent_report.html"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range modTempls {
|
||||||
|
modTempls[idx].indexFile = filepath.Join(TemplateDir, "admin_index.html")
|
||||||
|
}
|
||||||
|
|
||||||
|
templs := make([]templateLoader, 0, len(modTempls)+len(pubTempls))
|
||||||
|
|
||||||
|
templs = append(templs, pubTempls...)
|
||||||
|
templs = append(templs, modTempls...)
|
||||||
|
|
||||||
|
for _, templ := range templs {
|
||||||
|
t := template.Must(template.New(templ.name).Funcs(FuncMap).ParseFiles(templ.indexFile, filepath.Join(TemplateDir, templ.file)))
|
||||||
|
t = template.Must(t.ParseGlob(filepath.Join(TemplateDir, "_*.html")))
|
||||||
*templ.templ = t
|
*templ.templ = t
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,7 @@ func (f *UploadForm) ExtractInfo(r *http.Request) error {
|
||||||
f.Name = util.TrimWhitespaces(f.Name)
|
f.Name = util.TrimWhitespaces(f.Name)
|
||||||
f.Description = p.Sanitize(util.TrimWhitespaces(f.Description))
|
f.Description = p.Sanitize(util.TrimWhitespaces(f.Description))
|
||||||
f.Magnet = util.TrimWhitespaces(f.Magnet)
|
f.Magnet = util.TrimWhitespaces(f.Magnet)
|
||||||
cache.Clear()
|
cache.Impl.ClearAll()
|
||||||
|
|
||||||
catsSplit := strings.Split(f.Category, "_")
|
catsSplit := strings.Split(f.Category, "_")
|
||||||
// need this to prevent out of index panics
|
// need this to prevent out of index panics
|
||||||
|
|
|
@ -156,19 +156,19 @@ func UserProfileFormHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if len(err) == 0 {
|
if len(err) == 0 {
|
||||||
modelHelper.BindValueForm(&b, r)
|
modelHelper.BindValueForm(&b, r)
|
||||||
if !userPermission.HasAdmin(currentUser) {
|
if !userPermission.HasAdmin(currentUser) {
|
||||||
b.Username = currentUser.Username
|
b.Username = userProfile.Username
|
||||||
b.Status = currentUser.Status
|
b.Status = userProfile.Status
|
||||||
} else {
|
} else {
|
||||||
if b.Status == 2 {
|
if userProfile.Status != b.Status && b.Status == 2 {
|
||||||
err["errors"] = append(err["errors"], "Elevating status to moderator is prohibited")
|
err["errors"] = append(err["errors"], "Elevating status to moderator is prohibited")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err = modelHelper.ValidateForm(&b, err)
|
err = modelHelper.ValidateForm(&b, err)
|
||||||
if len(err) == 0 {
|
if len(err) == 0 {
|
||||||
if b.Email != currentUser.Email {
|
if b.Email != userProfile.Email {
|
||||||
userService.SendVerificationToUser(*currentUser, b.Email)
|
userService.SendVerificationToUser(*currentUser, b.Email)
|
||||||
infos["infos"] = append(infos["infos"], fmt.Sprintf(T("email_changed"), b.Email))
|
infos["infos"] = append(infos["infos"], fmt.Sprintf(T("email_changed"), b.Email))
|
||||||
b.Email = currentUser.Email // reset, it will be set when user clicks verification
|
b.Email = userProfile.Email // reset, it will be set when user clicks verification
|
||||||
}
|
}
|
||||||
userProfile, _, errorUser = userService.UpdateUser(w, &b, currentUser, id)
|
userProfile, _, errorUser = userService.UpdateUser(w, &b, currentUser, id)
|
||||||
if errorUser != nil {
|
if errorUser != nil {
|
||||||
|
|
|
@ -39,17 +39,19 @@ func PostCommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
id := vars["id"]
|
id := vars["id"]
|
||||||
|
|
||||||
if strings.TrimSpace(r.FormValue("comment")) == "" {
|
|
||||||
http.Error(w, "comment empty", 406)
|
|
||||||
}
|
|
||||||
|
|
||||||
userCaptcha := captcha.Extract(r)
|
userCaptcha := captcha.Extract(r)
|
||||||
if !captcha.Authenticate(userCaptcha) {
|
if !captcha.Authenticate(userCaptcha) {
|
||||||
http.Error(w, "bad captcha", 403)
|
http.Error(w, "bad captcha", 403)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
currentUser := GetUser(r)
|
currentUser := GetUser(r)
|
||||||
content := p.Sanitize(r.FormValue("comment"))
|
content := p.Sanitize(r.FormValue("comment"))
|
||||||
|
|
||||||
|
if strings.TrimSpace(content) == "" {
|
||||||
|
http.Error(w, "comment empty", 406)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
idNum, err := strconv.Atoi(id)
|
idNum, err := strconv.Atoi(id)
|
||||||
|
|
||||||
userID := currentUser.ID
|
userID := currentUser.ID
|
||||||
|
@ -76,6 +78,7 @@ func ReportTorrentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
userCaptcha := captcha.Extract(r)
|
userCaptcha := captcha.Extract(r)
|
||||||
if !captcha.Authenticate(userCaptcha) {
|
if !captcha.Authenticate(userCaptcha) {
|
||||||
http.Error(w, "bad captcha", 403)
|
http.Error(w, "bad captcha", 403)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
currentUser := GetUser(r)
|
currentUser := GetUser(r)
|
||||||
|
|
||||||
|
|
|
@ -28,15 +28,42 @@ func (b *Bucket) NewTransaction(swarms []model.Torrent) (t *Transaction) {
|
||||||
t = &Transaction{
|
t = &Transaction{
|
||||||
TransactionID: id,
|
TransactionID: id,
|
||||||
bucket: b,
|
bucket: b,
|
||||||
swarms: swarms,
|
swarms: make([]model.Torrent, len(swarms)),
|
||||||
state: stateSendID,
|
state: stateSendID,
|
||||||
}
|
}
|
||||||
|
copy(t.swarms[:], swarms[:])
|
||||||
b.transactions[id] = t
|
b.transactions[id] = t
|
||||||
b.access.Unlock()
|
b.access.Unlock()
|
||||||
return
|
return
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Bucket) ForEachTransaction(v func(uint32, *Transaction)) {
|
||||||
|
|
||||||
|
clone := make(map[uint32]*Transaction)
|
||||||
|
|
||||||
|
b.access.Lock()
|
||||||
|
|
||||||
|
for k := range b.transactions {
|
||||||
|
clone[k] = b.transactions[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
b.access.Unlock()
|
||||||
|
|
||||||
|
for k := range clone {
|
||||||
|
v(k, clone[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bucket) Forget(tid uint32) {
|
||||||
|
b.access.Lock()
|
||||||
|
_, ok := b.transactions[tid]
|
||||||
|
if ok {
|
||||||
|
delete(b.transactions, tid)
|
||||||
|
}
|
||||||
|
b.access.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Bucket) VisitTransaction(tid uint32, v func(*Transaction)) {
|
func (b *Bucket) VisitTransaction(tid uint32, v func(*Transaction)) {
|
||||||
b.access.Lock()
|
b.access.Lock()
|
||||||
t, ok := b.transactions[tid]
|
t, ok := b.transactions[tid]
|
||||||
|
|
|
@ -13,15 +13,20 @@ import (
|
||||||
// MTU yes this is the ipv6 mtu
|
// MTU yes this is the ipv6 mtu
|
||||||
const MTU = 1500
|
const MTU = 1500
|
||||||
|
|
||||||
|
// max number of scrapes per packet
|
||||||
|
const ScrapesPerPacket = 74
|
||||||
|
|
||||||
// bittorrent scraper
|
// bittorrent scraper
|
||||||
type Scraper struct {
|
type Scraper struct {
|
||||||
done chan int
|
done chan int
|
||||||
sendQueue chan *SendEvent
|
sendQueue chan *SendEvent
|
||||||
recvQueue chan *RecvEvent
|
recvQueue chan *RecvEvent
|
||||||
errQueue chan error
|
errQueue chan error
|
||||||
trackers map[string]*Bucket
|
trackers map[string]*Bucket
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
interval time.Duration
|
cleanup *time.Ticker
|
||||||
|
interval time.Duration
|
||||||
|
PacketsPerSecond uint
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(conf *config.ScraperConfig) (sc *Scraper, err error) {
|
func New(conf *config.ScraperConfig) (sc *Scraper, err error) {
|
||||||
|
@ -31,9 +36,15 @@ func New(conf *config.ScraperConfig) (sc *Scraper, err error) {
|
||||||
recvQueue: make(chan *RecvEvent, 1024),
|
recvQueue: make(chan *RecvEvent, 1024),
|
||||||
errQueue: make(chan error),
|
errQueue: make(chan error),
|
||||||
trackers: make(map[string]*Bucket),
|
trackers: make(map[string]*Bucket),
|
||||||
ticker: time.NewTicker(time.Second),
|
ticker: time.NewTicker(time.Second * 10),
|
||||||
interval: time.Second * time.Duration(conf.IntervalSeconds),
|
interval: time.Second * time.Duration(conf.IntervalSeconds),
|
||||||
|
cleanup: time.NewTicker(time.Minute),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sc.PacketsPerSecond == 0 {
|
||||||
|
sc.PacketsPerSecond = 10
|
||||||
|
}
|
||||||
|
|
||||||
for idx := range conf.Trackers {
|
for idx := range conf.Trackers {
|
||||||
err = sc.AddTracker(&conf.Trackers[idx])
|
err = sc.AddTracker(&conf.Trackers[idx])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -144,29 +155,55 @@ func (sc *Scraper) RunWorker(pc net.PacketConn) (err error) {
|
||||||
|
|
||||||
func (sc *Scraper) Run() {
|
func (sc *Scraper) Run() {
|
||||||
for {
|
for {
|
||||||
<-sc.ticker.C
|
select {
|
||||||
sc.Scrape()
|
case <-sc.ticker.C:
|
||||||
|
sc.Scrape(sc.PacketsPerSecond)
|
||||||
|
break
|
||||||
|
case <-sc.cleanup.C:
|
||||||
|
sc.removeStale()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sc *Scraper) Scrape() {
|
func (sc *Scraper) removeStale() {
|
||||||
now := time.Now().Add(0 - sc.interval)
|
|
||||||
|
|
||||||
rows, err := db.ORM.Raw("SELECT torrent_id, torrent_hash FROM torrents WHERE last_scrape IS NULL OR last_scrape < ? ORDER BY torrent_id DESC LIMIT 700", now).Rows()
|
for k := range sc.trackers {
|
||||||
|
sc.trackers[k].ForEachTransaction(func(tid uint32, t *Transaction) {
|
||||||
|
if t == nil || t.IsTimedOut() {
|
||||||
|
sc.trackers[k].Forget(tid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *Scraper) Scrape(packets uint) {
|
||||||
|
now := time.Now().Add(0 - sc.interval)
|
||||||
|
// only scrape torretns uploaded within 90 days
|
||||||
|
oldest := now.Add(0 - (time.Hour * 24 * 90))
|
||||||
|
rows, err := db.ORM.Raw("SELECT torrent_id, torrent_hash FROM torrents WHERE ( last_scrape IS NULL OR last_scrape < ? ) AND date > ? ORDER BY torrent_id DESC LIMIT ?", now, oldest, packets*ScrapesPerPacket).Rows()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
counter := 0
|
counter := 0
|
||||||
var scrape [70]model.Torrent
|
var scrape [ScrapesPerPacket]model.Torrent
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
idx := counter % 70
|
idx := counter % ScrapesPerPacket
|
||||||
rows.Scan(&scrape[idx].ID, &scrape[idx].Hash)
|
rows.Scan(&scrape[idx].ID, &scrape[idx].Hash)
|
||||||
counter++
|
counter++
|
||||||
if idx == 0 {
|
if counter%ScrapesPerPacket == 0 {
|
||||||
for _, b := range sc.trackers {
|
for _, b := range sc.trackers {
|
||||||
t := b.NewTransaction(scrape[:])
|
t := b.NewTransaction(scrape[:])
|
||||||
sc.sendQueue <- t.SendEvent(b.Addr)
|
sc.sendQueue <- t.SendEvent(b.Addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
idx := counter % ScrapesPerPacket
|
||||||
|
if idx > 0 {
|
||||||
|
for _, b := range sc.trackers {
|
||||||
|
t := b.NewTransaction(scrape[:idx])
|
||||||
|
sc.sendQueue <- t.SendEvent(b.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Infof("scrape %d", counter)
|
||||||
rows.Close()
|
rows.Close()
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,6 +11,9 @@ import (
|
||||||
"github.com/ewhal/nyaa/util/log"
|
"github.com/ewhal/nyaa/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TransactionTimeout 30 second timeout for transactions
|
||||||
|
const TransactionTimeout = time.Second * 30
|
||||||
|
|
||||||
const stateSendID = 0
|
const stateSendID = 0
|
||||||
const stateRecvID = 1
|
const stateRecvID = 1
|
||||||
const stateTransact = 2
|
const stateTransact = 2
|
||||||
|
@ -27,13 +30,12 @@ type Transaction struct {
|
||||||
bucket *Bucket
|
bucket *Bucket
|
||||||
state uint8
|
state uint8
|
||||||
swarms []model.Torrent
|
swarms []model.Torrent
|
||||||
|
lastData time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done marks this transaction as done and removes it from parent
|
// Done marks this transaction as done and removes it from parent
|
||||||
func (t *Transaction) Done() {
|
func (t *Transaction) Done() {
|
||||||
t.bucket.access.Lock()
|
t.bucket.Forget(t.TransactionID)
|
||||||
delete(t.bucket.transactions, t.TransactionID)
|
|
||||||
t.bucket.access.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Transaction) handleScrapeReply(data []byte) {
|
func (t *Transaction) handleScrapeReply(data []byte) {
|
||||||
|
@ -51,18 +53,22 @@ func (t *Transaction) handleScrapeReply(data []byte) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pgQuery = "UPDATE torrents SET seeders = $1 , leechers = $2 , completed = $3 , last_scrape = $4 WHERE torrent_id = $5"
|
||||||
|
const sqliteQuery = "UPDATE torrents SET seeders = ? , leechers = ? , completed = ? , last_scrape = ? WHERE torrent_id = ?"
|
||||||
|
|
||||||
// Sync syncs models with database
|
// Sync syncs models with database
|
||||||
func (t *Transaction) Sync() (err error) {
|
func (t *Transaction) Sync() (err error) {
|
||||||
for idx := range t.swarms {
|
q := pgQuery
|
||||||
err = db.ORM.Model(&t.swarms[idx]).Updates(map[string]interface{}{
|
if db.IsSqlite {
|
||||||
"seeders": t.swarms[idx].Seeders,
|
q = sqliteQuery
|
||||||
"leechers": t.swarms[idx].Leechers,
|
}
|
||||||
"completed": t.swarms[idx].Completed,
|
tx, e := db.ORM.DB().Begin()
|
||||||
"last_scrape": t.swarms[idx].LastScrape,
|
err = e
|
||||||
}).Error
|
if err == nil {
|
||||||
if err != nil {
|
for idx := range t.swarms {
|
||||||
break
|
_, err = tx.Exec(q, t.swarms[idx].Seeders, t.swarms[idx].Leechers, t.swarms[idx].Completed, t.swarms[idx].LastScrape, t.swarms[idx].ID)
|
||||||
}
|
}
|
||||||
|
tx.Commit()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -95,6 +101,7 @@ func (t *Transaction) SendEvent(to net.Addr) (ev *SendEvent) {
|
||||||
binary.BigEndian.PutUint32(ev.Data[12:], t.TransactionID)
|
binary.BigEndian.PutUint32(ev.Data[12:], t.TransactionID)
|
||||||
t.state = stateRecvID
|
t.state = stateRecvID
|
||||||
}
|
}
|
||||||
|
t.lastData = time.Now()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +111,7 @@ func (t *Transaction) handleError(msg string) {
|
||||||
|
|
||||||
// handle data for transaction
|
// handle data for transaction
|
||||||
func (t *Transaction) GotData(data []byte) (done bool) {
|
func (t *Transaction) GotData(data []byte) (done bool) {
|
||||||
|
t.lastData = time.Now()
|
||||||
if len(data) > 4 {
|
if len(data) > 4 {
|
||||||
cmd := binary.BigEndian.Uint32(data)
|
cmd := binary.BigEndian.Uint32(data)
|
||||||
switch cmd {
|
switch cmd {
|
||||||
|
@ -132,3 +139,8 @@ func (t *Transaction) GotData(data []byte) (done bool) {
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) IsTimedOut() bool {
|
||||||
|
return t.lastData.Add(TransactionTimeout).Before(time.Now())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -5,96 +5,98 @@ import (
|
||||||
"github.com/ewhal/nyaa/db"
|
"github.com/ewhal/nyaa/db"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
formStruct "github.com/ewhal/nyaa/service/user/form"
|
formStruct "github.com/ewhal/nyaa/service/user/form"
|
||||||
"github.com/ewhal/nyaa/util/log"
|
|
||||||
"github.com/ewhal/nyaa/util/modelHelper"
|
"github.com/ewhal/nyaa/util/modelHelper"
|
||||||
|
"github.com/ewhal/nyaa/util/timeHelper"
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const CookieName = "session"
|
||||||
|
|
||||||
|
// If you want to keep login cookies between restarts you need to make these permanent
|
||||||
var cookieHandler = securecookie.New(
|
var cookieHandler = securecookie.New(
|
||||||
securecookie.GenerateRandomKey(64),
|
securecookie.GenerateRandomKey(64),
|
||||||
securecookie.GenerateRandomKey(32))
|
securecookie.GenerateRandomKey(32))
|
||||||
|
|
||||||
func Token(r *http.Request) (string, error) {
|
// Encoding & Decoding of the cookie value
|
||||||
var token string
|
func DecodeCookie(cookie_value string) (uint, error) {
|
||||||
cookie, err := r.Cookie("session")
|
value := make(map[string]string)
|
||||||
|
err := cookieHandler.Decode(CookieName, cookie_value, &value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return token, err
|
return 0, err
|
||||||
}
|
}
|
||||||
cookieValue := make(map[string]string)
|
time_int, _ := strconv.ParseInt(value["t"], 10, 0)
|
||||||
err = cookieHandler.Decode("session", cookie.Value, &cookieValue)
|
if timeHelper.IsExpired(time.Unix(time_int, 0)) {
|
||||||
if err != nil {
|
return 0, errors.New("Cookie is expired")
|
||||||
return token, err
|
|
||||||
}
|
}
|
||||||
token = cookieValue["token"]
|
ret, err := strconv.ParseUint(value["u"], 10, 0)
|
||||||
if len(token) == 0 {
|
return uint(ret), err
|
||||||
return token, errors.New("token is empty")
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCookie sets a cookie.
|
func EncodeCookie(user_id uint) (string, error) {
|
||||||
func SetCookie(w http.ResponseWriter, token string) (int, error) {
|
validUntil := timeHelper.FewDaysLater(7) // 1 week
|
||||||
value := map[string]string{
|
value := map[string]string{
|
||||||
"token": token,
|
"u": strconv.FormatUint(uint64(user_id), 10),
|
||||||
|
"t": strconv.FormatInt(validUntil.Unix(), 10),
|
||||||
}
|
}
|
||||||
encoded, err := cookieHandler.Encode("session", value)
|
return cookieHandler.Encode(CookieName, value)
|
||||||
if err != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
cookie := &http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
Value: encoded,
|
|
||||||
Path: "/",
|
|
||||||
}
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearCookie clears a cookie.
|
|
||||||
func ClearCookie(w http.ResponseWriter) (int, error) {
|
func ClearCookie(w http.ResponseWriter) (int, error) {
|
||||||
cookie := &http.Cookie{
|
cookie := &http.Cookie{
|
||||||
Name: "session",
|
Name: CookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
}
|
}
|
||||||
http.SetCookie(w, cookie)
|
http.SetCookie(w, cookie)
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCookieHandler sets a cookie with email and password.
|
// SetCookieHandler sets the authentication cookie
|
||||||
func SetCookieHandler(w http.ResponseWriter, email string, pass string) (int, error) {
|
func SetCookieHandler(w http.ResponseWriter, email string, pass string) (int, error) {
|
||||||
if email != "" && pass != "" {
|
if email == "" || pass == "" {
|
||||||
var user model.User
|
return http.StatusNotFound, errors.New("No username/password entered")
|
||||||
isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
|
|
||||||
if isValidEmail {
|
|
||||||
log.Debug("User entered valid email.")
|
|
||||||
if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
|
|
||||||
return http.StatusNotFound, errors.New("User not found")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Debug("User entered username.")
|
|
||||||
if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
|
|
||||||
return http.StatusNotFound, errors.New("User not found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
|
|
||||||
if err != nil {
|
|
||||||
return http.StatusUnauthorized, errors.New("Password incorrect")
|
|
||||||
}
|
|
||||||
if user.Status == -1 {
|
|
||||||
return http.StatusUnauthorized, errors.New("Account banned")
|
|
||||||
}
|
|
||||||
status, err := SetCookie(w, user.Token)
|
|
||||||
if err != nil {
|
|
||||||
return status, err
|
|
||||||
}
|
|
||||||
w.Header().Set("X-Auth-Token", user.Token)
|
|
||||||
return http.StatusOK, nil
|
|
||||||
}
|
}
|
||||||
return http.StatusNotFound, errors.New("user not found")
|
|
||||||
|
var user model.User
|
||||||
|
// search by email or username
|
||||||
|
isValidEmail, _ := formStruct.EmailValidation(email, formStruct.NewErrors())
|
||||||
|
if isValidEmail {
|
||||||
|
if db.ORM.Where("email = ?", email).First(&user).RecordNotFound() {
|
||||||
|
return http.StatusNotFound, errors.New("User not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if db.ORM.Where("username = ?", email).First(&user).RecordNotFound() {
|
||||||
|
return http.StatusNotFound, errors.New("User not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(pass))
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusUnauthorized, errors.New("Password incorrect")
|
||||||
|
}
|
||||||
|
if user.Status == -1 {
|
||||||
|
return http.StatusUnauthorized, errors.New("Account banned")
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded, err := EncodeCookie(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: CookieName,
|
||||||
|
Value: encoded,
|
||||||
|
Path: "/",
|
||||||
|
HttpOnly: true,
|
||||||
|
}
|
||||||
|
http.SetCookie(w, cookie)
|
||||||
|
// also set response header for convenience
|
||||||
|
w.Header().Set("X-Auth-Token", encoded)
|
||||||
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterHanderFromForm sets cookie from a RegistrationForm.
|
// RegisterHanderFromForm sets cookie from a RegistrationForm.
|
||||||
|
@ -111,24 +113,31 @@ func RegisterHandler(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
return RegisterHanderFromForm(w, registrationForm)
|
return RegisterHanderFromForm(w, registrationForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentUser get a current user.
|
// CurrentUser determines the current user from the request
|
||||||
func CurrentUser(r *http.Request) (model.User, error) {
|
func CurrentUser(r *http.Request) (model.User, error) {
|
||||||
var user model.User
|
var user model.User
|
||||||
var token string
|
var encoded string
|
||||||
var err error
|
|
||||||
token = r.Header.Get("X-Auth-Token")
|
encoded = r.Header.Get("X-Auth-Token")
|
||||||
if len(token) > 0 {
|
if len(encoded) == 0 {
|
||||||
log.Debug("header token exists")
|
// check cookie instead
|
||||||
} else {
|
cookie, err := r.Cookie(CookieName)
|
||||||
token, err = Token(r)
|
|
||||||
log.Debug("header token does not exist")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
|
encoded = cookie.Value
|
||||||
}
|
}
|
||||||
if db.ORM.Where("api_token = ?", token).First(&user).RecordNotFound() {
|
user_id, err := DecodeCookie(encoded)
|
||||||
return user, errors.New("user not found")
|
if err != nil {
|
||||||
|
return user, err
|
||||||
}
|
}
|
||||||
err = db.ORM.Model(&user).Error
|
if db.ORM.Where("user_id = ?", user_id).First(&user).RecordNotFound() {
|
||||||
return user, err
|
return user, errors.New("User not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == -1 {
|
||||||
|
// recheck as user might've been banned in the meantime
|
||||||
|
return user, errors.New("Account banned")
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ewhal/nyaa/config"
|
|
||||||
"github.com/ewhal/nyaa/db"
|
"github.com/ewhal/nyaa/db"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
formStruct "github.com/ewhal/nyaa/service/user/form"
|
formStruct "github.com/ewhal/nyaa/service/user/form"
|
||||||
|
@ -15,7 +14,6 @@ import (
|
||||||
"github.com/ewhal/nyaa/util/crypto"
|
"github.com/ewhal/nyaa/util/crypto"
|
||||||
"github.com/ewhal/nyaa/util/log"
|
"github.com/ewhal/nyaa/util/log"
|
||||||
"github.com/ewhal/nyaa/util/modelHelper"
|
"github.com/ewhal/nyaa/util/modelHelper"
|
||||||
"github.com/ewhal/nyaa/util/timeHelper"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -69,15 +67,8 @@ func CreateUserFromForm(registrationForm formStruct.RegistrationForm) (model.Use
|
||||||
return user, err
|
return user, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
token, err := crypto.GenerateRandomToken32()
|
|
||||||
if err != nil {
|
|
||||||
return user, errors.New("token not generated")
|
|
||||||
}
|
|
||||||
user.Email = "" // unset email because it will be verified later
|
user.Email = "" // unset email because it will be verified later
|
||||||
|
|
||||||
user.Token = token
|
|
||||||
user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
|
|
||||||
log.Debugf("user %+v\n", user)
|
|
||||||
if db.ORM.Create(&user).Error != nil {
|
if db.ORM.Create(&user).Error != nil {
|
||||||
return user, errors.New("user not created")
|
return user, errors.New("user not created")
|
||||||
}
|
}
|
||||||
|
@ -157,17 +148,13 @@ func UpdateUserCore(user *model.User) (int, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := crypto.GenerateRandomToken32()
|
user.UpdatedAt = time.Now()
|
||||||
|
err := db.ORM.Save(user).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return http.StatusInternalServerError, err
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
user.Token = token
|
|
||||||
user.TokenExpiration = timeHelper.FewDaysLater(config.AuthTokenExpirationDay)
|
|
||||||
if db.ORM.Save(user).Error != nil {
|
|
||||||
return http.StatusInternalServerError, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user.UpdatedAt = time.Now()
|
|
||||||
return http.StatusOK, nil
|
return http.StatusOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,18 +184,13 @@ func UpdateUser(w http.ResponseWriter, form *formStruct.UserForm, currentUser *m
|
||||||
form.Username = user.Username
|
form.Username = user.Username
|
||||||
}
|
}
|
||||||
if (form.Email != user.Email) {
|
if (form.Email != user.Email) {
|
||||||
|
// send verification to new email and keep old
|
||||||
SendVerificationToUser(user, form.Email)
|
SendVerificationToUser(user, form.Email)
|
||||||
form.Email = user.Email
|
form.Email = user.Email
|
||||||
}
|
}
|
||||||
log.Debugf("form %+v\n", form)
|
log.Debugf("form %+v\n", form)
|
||||||
modelHelper.AssignValue(&user, form)
|
modelHelper.AssignValue(&user, form)
|
||||||
status, err := UpdateUserCore(&user)
|
status, err := UpdateUserCore(&user)
|
||||||
if err != nil {
|
|
||||||
return user, status, err
|
|
||||||
}
|
|
||||||
if userPermission.CurrentUserIdentical(currentUser, user.ID) {
|
|
||||||
status, err = SetCookie(w, user.Token)
|
|
||||||
}
|
|
||||||
return user, status, err
|
return user, status, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,6 @@ udp://tracker.leechers-paradise.org:6969
|
||||||
udp://explodie.org:6969
|
udp://explodie.org:6969
|
||||||
udp://tracker.opentrackr.org:1337
|
udp://tracker.opentrackr.org:1337
|
||||||
udp://tracker.internetwarriors.net:1337/announce
|
udp://tracker.internetwarriors.net:1337/announce
|
||||||
udp://eddie4.nl:6969/announce
|
|
||||||
http://mgtracker.org:6969/announce
|
http://mgtracker.org:6969/announce
|
||||||
http://tracker.baka-sub.cf/announce</pre>
|
http://tracker.baka-sub.cf/announce</pre>
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<input class="form-control" type="text" name="email" id="email" value="{{.Email}}">
|
<input class="form-control" type="text" name="email" id="email" value="{{.Email}}">
|
||||||
{{ range (index $.FormErrors "email")}}
|
{{ range (index $.FormErrors "email")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{{ range (index $.FormErrors "language")}}
|
{{ range (index $.FormErrors "language")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<input class="form-control" name="current_password" id="current_password" type="password">
|
<input class="form-control" name="current_password" id="current_password" type="password">
|
||||||
{{ range (index $.FormErrors "current_password")}}
|
{{ range (index $.FormErrors "current_password")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<input class="form-control" name="password" id="password" type="password">
|
<input class="form-control" name="password" id="password" type="password">
|
||||||
{{ range (index $.FormErrors "password")}}
|
{{ range (index $.FormErrors "password")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,7 +59,7 @@
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<input class="form-control" name="password_confirmation" id="password_confirmation" type="password">
|
<input class="form-control" name="password_confirmation" id="password_confirmation" type="password">
|
||||||
{{ range (index $.FormErrors "password_confirmation")}}
|
{{ range (index $.FormErrors "password_confirmation")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<input class="form-control" name="username" id="username" type="text" value="{{.Username}}">
|
<input class="form-control" name="username" id="username" type="text" value="{{.Username}}">
|
||||||
{{ range (index $.FormErrors "username")}}
|
{{ range (index $.FormErrors "username")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,12 +81,14 @@
|
||||||
<select id="status" name="status" class="form-control">
|
<select id="status" name="status" class="form-control">
|
||||||
<option value="-1" {{ if eq .Status -1 }}selected{{end}}>{{ T "banned"}}</option>
|
<option value="-1" {{ if eq .Status -1 }}selected{{end}}>{{ T "banned"}}</option>
|
||||||
<option value="0" {{ if eq .Status 0 }}selected{{end}}>{{ T "member"}} ({{ T "default" }})</option>
|
<option value="0" {{ if eq .Status 0 }}selected{{end}}>{{ T "member"}} ({{ T "default" }})</option>
|
||||||
<option value="1" {{ if eq .Status 1 }}selected{{end}}>{{ T "trusted_member"}} </option>
|
<option value="1" {{ if eq .Status 1 }}selected{{end}}>{{ T "trusted_member"}}</option>
|
||||||
<!-- <option value="2" {{ if eq .Status 2 }}selected{{end}}>{{ T "moderator"}} </option> -->
|
{{ if eq .Status 2 }} <!-- just so that it shows correctly -->
|
||||||
|
<option value="2" selected>{{ T "moderator"}}</option>
|
||||||
|
{{end}}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{{ range (index $.FormErrors "status")}}
|
{{ range (index $.FormErrors "status")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -26,7 +26,18 @@
|
||||||
|
|
||||||
<!-- Website CSS -->
|
<!-- Website CSS -->
|
||||||
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
|
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Night mode
|
||||||
|
var night = localStorage.getItem("night");
|
||||||
|
var darkStyleLink = document.createElement('link');
|
||||||
|
darkStyleLink.id = "style-dark";
|
||||||
|
darkStyleLink.rel = "stylesheet";
|
||||||
|
darkStyleLink.type = "text/css";
|
||||||
|
darkStyleLink.href = "/css/style-night.css"
|
||||||
|
if (night == "true") {
|
||||||
|
document.getElementsByTagName("head")[0].append(darkStyleLink);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-default" id="mainmenu">
|
<nav class="navbar navbar-default" id="mainmenu">
|
||||||
|
@ -77,7 +88,6 @@
|
||||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||||
<script type="text/javascript" charset="utf-8" src="{{.URL.Parse "/js/main.js"}}"></script>
|
<script type="text/javascript" charset="utf-8" src="{{.URL.Parse "/js/main.js"}}"></script>
|
||||||
|
|
||||||
|
|
||||||
{{block "js_footer" .}}{{end}}
|
{{block "js_footer" .}}{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
|
||||||
<title>Nyaa Pantsu - {{block "title" .}}{{ T "error_404" }}{{end}}</title>
|
<title>Nyaa Pantsu - {{block "title" .}}{{ T "error_404" }}{{end}}</title>
|
||||||
<link rel="icon" type="image/png" href="/img/favicon.png?v=3" />
|
<link rel="icon" type="image/png" href="/img/favicon.png?v=3" />
|
||||||
|
|
||||||
<!-- RSS Feed with Context -->
|
<!-- RSS Feed with Context -->
|
||||||
|
@ -30,6 +30,18 @@
|
||||||
|
|
||||||
<!-- Website CSS -->
|
<!-- Website CSS -->
|
||||||
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
|
<link rel="stylesheet" id="style" href="{{.URL.Parse "/css/style.css"}}">
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Night mode
|
||||||
|
var night = localStorage.getItem("night");
|
||||||
|
var darkStyleLink = document.createElement('link');
|
||||||
|
darkStyleLink.id = "style-dark";
|
||||||
|
darkStyleLink.rel = "stylesheet";
|
||||||
|
darkStyleLink.type = "text/css";
|
||||||
|
darkStyleLink.href = "/css/style-night.css"
|
||||||
|
if (night == "true") {
|
||||||
|
document.getElementsByTagName("head")[0].append(darkStyleLink);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<nav class="navbar navbar-default" id="mainmenu">
|
<nav class="navbar navbar-default" id="mainmenu">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{{define "title"}}{{ T "sign_in_title" }}{{end}}
|
{{define "title"}}{{ T "sign_in_title" }}{{end}}
|
||||||
{{define "contclass"}}cont-view{{end}}
|
{{define "contclass"}}cont-view{{end}}
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<div class="blockBody">
|
<div class="blockBody">
|
||||||
|
@ -12,20 +12,20 @@
|
||||||
<div class="alert alert-danger">{{ . }}</div>
|
<div class="alert alert-danger">{{ . }}</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" name="username" id="username" class="form-control input-lg" placeholder="{{ T "email_address_or_username"}}">
|
<input type="text" name="username" id="username" class="form-control input-lg" autofocus="" placeholder="{{ T "email_address_or_username"}}">
|
||||||
{{ range (index $.FormErrors "username")}}
|
{{ range (index $.FormErrors "username")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{ T "password"}}">
|
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{ T "password"}}">
|
||||||
{{ range (index $.FormErrors "password")}}
|
{{ range (index $.FormErrors "password")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<span class="button-checkbox">
|
<span class="button-checkbox">
|
||||||
<button type="button" class="btn hidden" data-color="info">{{ T "remember_me"}}</button>
|
<!-- <button type="button" class="btn hidden" data-color="info">{{ T "remember_me"}}</button>
|
||||||
<input type="checkbox" name="remember_me" id="remember_me" checked="checked">
|
<input type="checkbox" name="remember_me" id="remember_me" checked="checked"> -->
|
||||||
<a href="" class="btn btn-link pull-right">{{ T "forgot_password"}}</a>
|
<a href="" class="btn btn-link pull-right">{{ T "forgot_password"}}</a>
|
||||||
</span>
|
</span>
|
||||||
<hr class="colorgraph">
|
<hr class="colorgraph">
|
||||||
|
|
|
@ -14,13 +14,13 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" name="username" id="display_name" class="form-control input-lg" placeholder="{{T "username" }}" tabindex="1" value="{{ .Username }}">
|
<input type="text" name="username" id="display_name" class="form-control input-lg" placeholder="{{T "username" }}" tabindex="1" value="{{ .Username }}">
|
||||||
{{ range (index $.FormErrors "username")}}
|
{{ range (index $.FormErrors "username")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="email" name="email" id="email" class="form-control input-lg" placeholder="{{T "email_address" }}" tabindex="2" value="{{ .Email }}">
|
<input type="email" name="email" id="email" class="form-control input-lg" placeholder="{{T "email_address" }}" tabindex="2" value="{{ .Email }}">
|
||||||
{{ range (index $.FormErrors "email")}}
|
{{ range (index $.FormErrors "email")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{T "password" }}" tabindex="3" value="{{ .Password }}">
|
<input type="password" name="password" id="password" class="form-control input-lg" placeholder="{{T "password" }}" tabindex="3" value="{{ .Password }}">
|
||||||
{{ range (index $.FormErrors "password")}}
|
{{ range (index $.FormErrors "password")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="password" name="password_confirmation" id="password_confirmation" class="form-control input-lg" placeholder="{{T "confirm_password" }}" tabindex="4">
|
<input type="password" name="password_confirmation" id="password_confirmation" class="form-control input-lg" placeholder="{{T "confirm_password" }}" tabindex="4">
|
||||||
{{ range (index $.FormErrors "password_confirmation")}}
|
{{ range (index $.FormErrors "password_confirmation")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,7 +47,7 @@
|
||||||
<button type="button" class="btn hidden" data-color="info" tabindex="5">{{T "i_agree" }}</button>
|
<button type="button" class="btn hidden" data-color="info" tabindex="5">{{T "i_agree" }}</button>
|
||||||
<input type="checkbox" name="t_and_c" id="t_and_c" value="1">
|
<input type="checkbox" name="t_and_c" id="t_and_c" value="1">
|
||||||
{{ range (index $.FormErrors "t_and_c")}}
|
{{ range (index $.FormErrors "t_and_c")}}
|
||||||
<p class="bg-danger">{{ . }}</p>
|
<p class="text-error">{{ . }}</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,37 +5,43 @@
|
||||||
{{with .Torrent}}
|
{{with .Torrent}}
|
||||||
<hr>
|
<hr>
|
||||||
<div class="content" style="margin-bottom: 2em;">
|
<div class="content" style="margin-bottom: 2em;">
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h3 style="word-break:break-all" {{if eq .Status 2}}class="remake" {{end}} {{if eq .Status 3}}class="trusted" {{end}} {{if eq .Status 4}}class="aplus"{{end}}>{{.Name}}</h3>
|
<h3 style="word-break:break-all" {{if eq .Status 2}}class="remake" {{end}} {{if eq .Status 3}}class="trusted" {{end}} {{if eq .Status 4}}class="aplus"{{end}}>{{.Name}}</h3>
|
||||||
<div class="uploaded_by">
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="row" id="description">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div style="float: left;">
|
||||||
|
<div class="uploaded_by">
|
||||||
<img style="float:left; margin-right: 1em;" src="{{$.URL.Parse (printf "/img/torrents/%s.png" .SubCategory) }}">
|
<img style="float:left; margin-right: 1em;" src="{{$.URL.Parse (printf "/img/torrents/%s.png" .SubCategory) }}">
|
||||||
<h4>Uploaded by <a href="{{$.URL.Parse (printf "/user/%d/-" .UploaderID) }}">{{.UploaderName}}</a></h4>
|
<h4>Uploaded by <a href="{{$.URL.Parse (printf "/user/%d/-" .UploaderID) }}">{{.UploaderName}}</a></h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div style="float:right;">
|
||||||
<br />
|
<a style="margin: 5px;" aria-label="Magnet Button" href="{{.Magnet}}" type="button" class="btn btn-lg btn-success">
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<a style="margin: 5px;" aria-label="Magnet Button" href="{{.Magnet}}" type="button" class="btn btn-lg btn-success download-btn">
|
|
||||||
<span class="glyphicon glyphicon-magnet" aria-hidden="true"></span> Download!
|
<span class="glyphicon glyphicon-magnet" aria-hidden="true"></span> Download!
|
||||||
</a>
|
</a>
|
||||||
{{if ne .TorrentLink ""}}
|
{{if ne .TorrentLink ""}}
|
||||||
<a style="margin: 5px;" aria-label="Torrent file" href="{{.TorrentLink}}" type="button" class="btn btn-lg btn-success download-btn">
|
<a style="margin: 5px;" aria-label="Torrent file" href="{{.TorrentLink}}" type="button" class="btn btn-lg btn-success">
|
||||||
<span class="glyphicon glyphicon-floppy-save" aria-hidden="true"></span> Torrent file
|
<span class="glyphicon glyphicon-floppy-save" aria-hidden="true"></span> Torrent file
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
<a style="margin: 5px;" aria-label="Report button" type="button" data-toggle="modal" data-target="#reportModal" class="btn btn-danger btn-lg">
|
<a style="margin: 5px;" aria-label="Report button" data-toggle="modal" data-target="#reportModal" class="btn btn-danger btn-sm">
|
||||||
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Report!
|
<span class="glyphicon glyphicon-remove" aria-hidden="true"></span> Report!
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{{ if HasAdmin $.User}}
|
{{ if HasAdmin $.User}}
|
||||||
<a href="{{ genRoute "mod_tdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i></a>
|
<a href="{{ genRoute "mod_tdelete" }}?id={{ .ID }}" class="btn btn-danger btn-lg" onclick="if (!confirm('Are you sure?')) return false;"><i class="glyphicon glyphicon-trash"></i></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div style="clear: both;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h4>{{T "description"}}</h4>
|
||||||
|
<div style="word-break:break-all;">{{.Description}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,12 +79,6 @@
|
||||||
<hr>
|
<hr>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" id="description">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<h4>{{T "description"}}</h4>
|
|
||||||
<div style="word-break:break-all;">{{.Description}}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" id="comments">
|
<div class="row" id="comments">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<h4>{{T "comments"}}</h4>
|
<h4>{{T "comments"}}</h4>
|
||||||
|
|
|
@ -590,5 +590,17 @@
|
||||||
{
|
{
|
||||||
"id": "torrent_status_remake",
|
"id": "torrent_status_remake",
|
||||||
"translation": "Remake"
|
"translation": "Remake"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seeders",
|
||||||
|
"translation": "Seeder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "leechers",
|
||||||
|
"translation": "Leecher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "completed",
|
||||||
|
"translation": "Komplett"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -606,5 +606,17 @@
|
||||||
{
|
{
|
||||||
"id":"date_format",
|
"id":"date_format",
|
||||||
"translation": "2006-01-02 15:04"
|
"translation": "2006-01-02 15:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seeders",
|
||||||
|
"translation": "Seeders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "leechers",
|
||||||
|
"translation": "Leechers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "completed",
|
||||||
|
"translation": "Completed"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -606,5 +606,21 @@
|
||||||
{
|
{
|
||||||
"id": "profile_edit_page",
|
"id": "profile_edit_page",
|
||||||
"translation": "Éditer le profil de %s"
|
"translation": "Éditer le profil de %s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"date_format",
|
||||||
|
"translation": "2006-01-02 15:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seeders",
|
||||||
|
"translation": "Seeders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "leechers",
|
||||||
|
"translation": "Leechers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "completed",
|
||||||
|
"translation": "Terminé"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -602,5 +602,21 @@
|
||||||
{
|
{
|
||||||
"id": "profile_edit_page",
|
"id": "profile_edit_page",
|
||||||
"translation": "Modifica il profilo di %s"
|
"translation": "Modifica il profilo di %s"
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"id":"date_format",
|
||||||
|
"translation": "2006-01-02 15:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seeders",
|
||||||
|
"translation": "Seeders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "leechers",
|
||||||
|
"translation": "Leechers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "completed",
|
||||||
|
"translation": "Completato"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -45,15 +45,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":"confirm_password",
|
"id":"confirm_password",
|
||||||
"translation": "パスワードを確認する"
|
"translation": "パスワードの再入力"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":"i_agree",
|
"id":"i_agree",
|
||||||
"translation": "同意します"
|
"translation": "同意する"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":"terms_conditions_confirm",
|
"id":"terms_conditions_confirm",
|
||||||
"translation": "<strong class=\"label label-primary\">登録する</strong> をクリックすることにより、Cookie の使用を含む、本サイトで規定されている <a href=\"#\" data-toggle=\"modal\" data-target=\"#t_and_c_m\"> 利用規約</a> に同意するものとします。"
|
"translation": "<strong class=\"label label-primary\">登録</strong> をクリックすることにより、Cookie の使用を含む、本サイトの <a href=\"#\" data-toggle=\"modal\" data-target=\"#t_and_c_m\">利用規約</a> に同意したものとみなします。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id":"signin",
|
"id":"signin",
|
||||||
|
@ -213,7 +213,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "notice_keep_seeding",
|
"id": "notice_keep_seeding",
|
||||||
"translation": "お願い:DHT 機能を有効にし、なるべくシードを継続してください"
|
"translation": "お願い: DHT 機能を有効にし、なるべくシードを継続してください"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "official_nyaapocalipse_faq",
|
"id": "official_nyaapocalipse_faq",
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "magnet_link_should_look_like",
|
"id": "magnet_link_should_look_like",
|
||||||
"translation": "magnet リンクは次のようになっているはずです:"
|
"translation": "magnet リンクは次のようになっているはずです:"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "which_trackers_do_you_recommend",
|
"id": "which_trackers_do_you_recommend",
|
||||||
|
@ -297,7 +297,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "answer_which_trackers_do_you_recommend",
|
"id": "answer_which_trackers_do_you_recommend",
|
||||||
"translation": "トラッカーに Torrent のアップロードを拒否された場合は、この中のいくつかを追加する必要があります:"
|
"translation": "トラッカーに Torrent のアップロードを拒否された場合は、この中のいくつかを追加する必要があります:"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "how_can_i_help",
|
"id": "how_can_i_help",
|
||||||
|
@ -321,7 +321,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "nyaa_pantsu_dont_host_files",
|
"id": "nyaa_pantsu_dont_host_files",
|
||||||
"translation": " nyaa.pantsu.cat と sukebei.pantsu.cat はいかなるファイルもホストしていません。"
|
"translation": "nyaa.pantsu.cat と sukebei.pantsu.cat はいかなるファイルもホストしていません。"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "upload_magnet",
|
"id": "upload_magnet",
|
||||||
|
@ -449,7 +449,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "filter_remakes",
|
"id": "filter_remakes",
|
||||||
"translation": "リメイクされたフィルター"
|
"translation": "再構成されたフィルター"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "trusted",
|
"id": "trusted",
|
||||||
|
@ -550,5 +550,49 @@
|
||||||
{
|
{
|
||||||
"id": "delete_success",
|
"id": "delete_success",
|
||||||
"translation": "アカウントが削除されました。"
|
"translation": "アカウントが削除されました。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "moderation",
|
||||||
|
"translation": "緩和"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "who_is_renchon",
|
||||||
|
"translation": "「れんちょん」って誰やねん"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "renchon_anon_explanation",
|
||||||
|
"translation": "「れんちょん」は匿名でアップロードもしくはコメントを書き込んだ際に割り当てられるユーザー名なのん。オリジナルのアップロード者と一緒に表示されることがあるけど、オリジナルの nyaa からインポートされた Torrent にも使用されるのん。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mark_as_remake",
|
||||||
|
"translation": "再構成としてマーク"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "email_changed",
|
||||||
|
"translation": "メールアドレスが変更されました。なお、送信されたリンクをクリックして認証を済ませる必要があります: %s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torrent_status",
|
||||||
|
"translation": "Torrent の状態"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torrent_status_hidden",
|
||||||
|
"translation": "非表示"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torrent_status_normal",
|
||||||
|
"translation": "通常"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torrent_status_remake",
|
||||||
|
"translation": "再構成"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "profile_edit_page",
|
||||||
|
"translation": "%s のプロフィールを編集"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"date_format",
|
||||||
|
"translation": "2006/01/02 15:04"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -602,5 +602,21 @@
|
||||||
{
|
{
|
||||||
"id": "profile_edit_page",
|
"id": "profile_edit_page",
|
||||||
"translation": "แก้ไขโปรไฟล์ของ %s"
|
"translation": "แก้ไขโปรไฟล์ของ %s"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"date_format",
|
||||||
|
"translation": "2006/01/02 15:04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "seeders",
|
||||||
|
"translation": "ผู้ปล่อย"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "leechers",
|
||||||
|
"translation": "ผู้โหลด"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "completed",
|
||||||
|
"translation": "โหลดเสร็จแล้ว"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,8 +2,8 @@ package languages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/nicksnyder/go-i18n/i18n"
|
|
||||||
"github.com/ewhal/nyaa/service/user"
|
"github.com/ewhal/nyaa/service/user"
|
||||||
|
"github.com/nicksnyder/go-i18n/i18n"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -22,7 +22,7 @@ func TfuncWithFallback(language string, languages ...string) (i18n.TranslateFunc
|
||||||
|
|
||||||
if err1 != nil && err2 != nil {
|
if err1 != nil && err2 != nil {
|
||||||
// fallbackT is still a valid function even with the error, it returns translationID.
|
// fallbackT is still a valid function even with the error, it returns translationID.
|
||||||
return fallbackT, err2;
|
return fallbackT, err2
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(translationID string, args ...interface{}) string {
|
return func(translationID string, args ...interface{}) string {
|
||||||
|
@ -42,7 +42,7 @@ func GetAvailableLanguages() (languages map[string]string) {
|
||||||
/* Translation files should have an ID with the translated language name.
|
/* Translation files should have an ID with the translated language name.
|
||||||
If they don't, just use the languageTag */
|
If they don't, just use the languageTag */
|
||||||
if languageName := T("language_name"); languageName != "language_name" {
|
if languageName := T("language_name"); languageName != "language_name" {
|
||||||
languages[languageTag] = languageName;
|
languages[languageTag] = languageName
|
||||||
} else {
|
} else {
|
||||||
languages[languageTag] = languageTag
|
languages[languageTag] = languageTag
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, default
|
||||||
userLanguage := ""
|
userLanguage := ""
|
||||||
user, _, err := userService.RetrieveCurrentUser(r)
|
user, _, err := userService.RetrieveCurrentUser(r)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
userLanguage = user.Language;
|
userLanguage = user.Language
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := r.Cookie("lang")
|
cookie, err := r.Cookie("lang")
|
||||||
|
@ -78,5 +78,6 @@ func SetTranslationFromRequest(tmpl *template.Template, r *http.Request, default
|
||||||
|
|
||||||
// go-i18n supports the format of the Accept-Language header, thankfully.
|
// go-i18n supports the format of the Accept-Language header, thankfully.
|
||||||
headerLanguage := r.Header.Get("Accept-Language")
|
headerLanguage := r.Header.Get("Accept-Language")
|
||||||
|
r.Header.Add("Vary", "Accept-Encoding")
|
||||||
return SetTranslation(tmpl, userLanguage, cookieLanguage, headerLanguage, defaultLanguage)
|
return SetTranslation(tmpl, userLanguage, cookieLanguage, headerLanguage, defaultLanguage)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/ewhal/nyaa/cache"
|
"github.com/ewhal/nyaa/cache"
|
||||||
"github.com/ewhal/nyaa/common"
|
"github.com/ewhal/nyaa/common"
|
||||||
|
"github.com/ewhal/nyaa/config"
|
||||||
"github.com/ewhal/nyaa/db"
|
"github.com/ewhal/nyaa/db"
|
||||||
"github.com/ewhal/nyaa/model"
|
"github.com/ewhal/nyaa/model"
|
||||||
"github.com/ewhal/nyaa/service"
|
"github.com/ewhal/nyaa/service"
|
||||||
|
@ -16,6 +17,18 @@ import (
|
||||||
"github.com/ewhal/nyaa/util/log"
|
"github.com/ewhal/nyaa/util/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var searchOperator string
|
||||||
|
|
||||||
|
func Configure(conf *config.SearchConfig) (err error) {
|
||||||
|
// SQLite has case-insensitive LIKE, but no ILIKE
|
||||||
|
if db.ORM.Dialect().GetName() == "sqlite3" {
|
||||||
|
searchOperator = "LIKE ?"
|
||||||
|
} else {
|
||||||
|
searchOperator = "ILIKE ?"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func SearchByQuery(r *http.Request, pagenum int) (search common.SearchParam, tor []model.Torrent, count int, err error) {
|
func SearchByQuery(r *http.Request, pagenum int) (search common.SearchParam, tor []model.Torrent, count int, err error) {
|
||||||
search, tor, count, err = searchByQuery(r, pagenum, true)
|
search, tor, count, err = searchByQuery(r, pagenum, true)
|
||||||
return
|
return
|
||||||
|
@ -40,7 +53,7 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
||||||
search.Page = pagenum
|
search.Page = pagenum
|
||||||
search.Query = r.URL.Query().Get("q")
|
search.Query = r.URL.Query().Get("q")
|
||||||
userID, _ := strconv.Atoi(r.URL.Query().Get("userID"))
|
userID, _ := strconv.Atoi(r.URL.Query().Get("userID"))
|
||||||
search.UserID = uint(userID)
|
search.UserID = uint(userID)
|
||||||
|
|
||||||
switch s := r.URL.Query().Get("s"); s {
|
switch s := r.URL.Query().Get("s"); s {
|
||||||
case "1":
|
case "1":
|
||||||
|
@ -75,22 +88,36 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
||||||
case "1":
|
case "1":
|
||||||
search.Sort = common.Name
|
search.Sort = common.Name
|
||||||
orderBy += "torrent_name"
|
orderBy += "torrent_name"
|
||||||
|
break
|
||||||
case "2":
|
case "2":
|
||||||
search.Sort = common.Date
|
search.Sort = common.Date
|
||||||
orderBy += "date"
|
orderBy += "date"
|
||||||
|
break
|
||||||
case "3":
|
case "3":
|
||||||
search.Sort = common.Downloads
|
search.Sort = common.Downloads
|
||||||
orderBy += "downloads"
|
orderBy += "downloads"
|
||||||
|
break
|
||||||
case "4":
|
case "4":
|
||||||
search.Sort = common.Size
|
search.Sort = common.Size
|
||||||
orderBy += "filesize"
|
orderBy += "filesize"
|
||||||
|
break
|
||||||
case "5":
|
case "5":
|
||||||
|
search.Sort = common.Seeders
|
||||||
orderBy += "seeders"
|
orderBy += "seeders"
|
||||||
|
search.NotNull += "seeders IS NOT NULL "
|
||||||
|
break
|
||||||
case "6":
|
case "6":
|
||||||
|
search.Sort = common.Leechers
|
||||||
orderBy += "leechers"
|
orderBy += "leechers"
|
||||||
|
search.NotNull += "leechers IS NOT NULL "
|
||||||
|
break
|
||||||
case "7":
|
case "7":
|
||||||
|
search.Sort = common.Completed
|
||||||
orderBy += "completed"
|
orderBy += "completed"
|
||||||
|
search.NotNull += "completed IS NOT NULL "
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
|
search.Sort = common.ID
|
||||||
orderBy += "torrent_id"
|
orderBy += "torrent_id"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,68 +130,68 @@ func searchByQuery(r *http.Request, pagenum int, countAll bool) (
|
||||||
default:
|
default:
|
||||||
orderBy += "desc"
|
orderBy += "desc"
|
||||||
}
|
}
|
||||||
|
parameters := serviceBase.WhereParams{
|
||||||
|
Params: make([]interface{}, 0, 64),
|
||||||
|
}
|
||||||
|
conditions := make([]string, 0, 64)
|
||||||
|
|
||||||
tor, count, err = cache.Get(search, func() (tor []model.Torrent, count int, err error) {
|
if search.Category.Main != 0 {
|
||||||
parameters := serviceBase.WhereParams{
|
conditions = append(conditions, "category = ?")
|
||||||
Params: make([]interface{}, 0, 64),
|
parameters.Params = append(parameters.Params, string(catString[0]))
|
||||||
|
}
|
||||||
|
if search.UserID != 0 {
|
||||||
|
conditions = append(conditions, "uploader = ?")
|
||||||
|
parameters.Params = append(parameters.Params, search.UserID)
|
||||||
|
}
|
||||||
|
if search.Category.Sub != 0 {
|
||||||
|
conditions = append(conditions, "sub_category = ?")
|
||||||
|
parameters.Params = append(parameters.Params, string(catString[2]))
|
||||||
|
}
|
||||||
|
if search.Status != 0 {
|
||||||
|
if search.Status == common.FilterRemakes {
|
||||||
|
conditions = append(conditions, "status <> ?")
|
||||||
|
} else {
|
||||||
|
conditions = append(conditions, "status >= ?")
|
||||||
}
|
}
|
||||||
conditions := make([]string, 0, 64)
|
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
|
||||||
if search.Category.Main != 0 {
|
}
|
||||||
conditions = append(conditions, "category = ?")
|
if len(search.NotNull) > 0 {
|
||||||
parameters.Params = append(parameters.Params, string(catString[0]))
|
conditions = append(conditions, search.NotNull)
|
||||||
}
|
}
|
||||||
if search.UserID != 0 {
|
|
||||||
conditions = append(conditions, "uploader = ?")
|
if len(search.NotNull) > 0 {
|
||||||
parameters.Params = append(parameters.Params, search.UserID)
|
conditions = append(conditions, search.NotNull)
|
||||||
}
|
}
|
||||||
if search.Category.Sub != 0 {
|
|
||||||
conditions = append(conditions, "sub_category = ?")
|
searchQuerySplit := strings.Fields(search.Query)
|
||||||
parameters.Params = append(parameters.Params, string(catString[2]))
|
for i, word := range searchQuerySplit {
|
||||||
}
|
firstRune, _ := utf8.DecodeRuneInString(word)
|
||||||
if search.Status != 0 {
|
if len(word) == 1 && unicode.IsPunct(firstRune) {
|
||||||
if search.Status == common.FilterRemakes {
|
// some queries have a single punctuation character
|
||||||
conditions = append(conditions, "status > ?")
|
// which causes a full scan instead of using the index
|
||||||
} else {
|
// and yields no meaningful results.
|
||||||
conditions = append(conditions, "status >= ?")
|
// due to len() == 1 we're just looking at 1-byte/ascii
|
||||||
}
|
// punctuation characters.
|
||||||
parameters.Params = append(parameters.Params, strconv.Itoa(int(search.Status)+1))
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuerySplit := strings.Fields(search.Query)
|
// TODO: make this faster ?
|
||||||
for i, word := range searchQuerySplit {
|
conditions = append(conditions, "torrent_name "+searchOperator)
|
||||||
firstRune, _ := utf8.DecodeRuneInString(word)
|
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
|
||||||
if len(word) == 1 && unicode.IsPunct(firstRune) {
|
}
|
||||||
// some queries have a single punctuation character
|
|
||||||
// which causes a full scan instead of using the index
|
|
||||||
// and yields no meaningful results.
|
|
||||||
// due to len() == 1 we're just looking at 1-byte/ascii
|
|
||||||
// punctuation characters.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// SQLite has case-insensitive LIKE, but no ILIKE
|
parameters.Conditions = strings.Join(conditions[:], " AND ")
|
||||||
var operator string
|
|
||||||
if db.ORM.Dialect().GetName() == "sqlite3" {
|
|
||||||
operator = "LIKE ?"
|
|
||||||
} else {
|
|
||||||
operator = "ILIKE ?"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make this faster ?
|
log.Infof("SQL query is :: %s\n", parameters.Conditions)
|
||||||
conditions = append(conditions, "torrent_name "+operator)
|
|
||||||
parameters.Params = append(parameters.Params, "%"+searchQuerySplit[i]+"%")
|
tor, count, err = cache.Impl.Get(search, func() (tor []model.Torrent, count int, err error) {
|
||||||
}
|
|
||||||
|
|
||||||
parameters.Conditions = strings.Join(conditions[:], " AND ")
|
|
||||||
log.Infof("SQL query is :: %s\n", parameters.Conditions)
|
|
||||||
if countAll {
|
if countAll {
|
||||||
tor, count, err = torrentService.GetTorrentsOrderBy(¶meters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
|
tor, count, err = torrentService.GetTorrentsOrderBy(¶meters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
|
||||||
} else {
|
} else {
|
||||||
tor, err = torrentService.GetTorrentsOrderByNoCount(¶meters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
|
tor, err = torrentService.GetTorrentsOrderByNoCount(¶meters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
})
|
})
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
Référencer dans un nouveau ticket