Albirew/nyaa-pantsu
Archivé
1
0
Bifurcation 0

Merge branch 'master' of github.com:ewhal/nyaa into api

Cette révision appartient à :
ayame-git 2017-05-12 00:59:35 +03:00
révision 0584c914bc
46 fichiers modifiés avec 1023 ajouts et 507 suppressions

Voir le fichier

@ -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

Voir le fichier

@ -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
Voir le fichier

@ -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
Voir le fichier

@ -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
Voir le fichier

@ -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
Voir le fichier

@ -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{}
}

Voir le fichier

@ -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
Voir le fichier

@ -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",
}

Voir le fichier

@ -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
Voir le fichier

@ -0,0 +1,6 @@
package config
type SearchConfig struct {
}
var DefaultSearchConfig = SearchConfig{}

Voir le fichier

@ -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"}

Voir le fichier

@ -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
} }

Voir le fichier

@ -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
Voir le fichier

@ -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)

Voir le fichier

@ -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.

Voir le fichier

@ -143,4 +143,9 @@ a:hover {
} }
.modal-content .close { .modal-content .close {
color: #fff; color: #fff;
} }
.text-error {
background: #29363d;
color: #cf9fff;
}

Voir le fichier

@ -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;
}

Voir le fichier

@ -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");
} }

Voir le fichier

@ -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)

Voir le fichier

@ -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.")
} }

Voir le fichier

@ -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)
} }

Voir le fichier

@ -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)

Voir le fichier

@ -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: &notFoundTemplate, templ: &notFoundTemplate,
@ -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
} }
} }

Voir le fichier

@ -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

Voir le fichier

@ -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 {

Voir le fichier

@ -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)

Voir le fichier

@ -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]

Voir le fichier

@ -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 {

Voir le fichier

@ -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())
}

Voir le fichier

@ -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
} }

Voir le fichier

@ -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
} }

Voir le fichier

@ -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>

Voir le fichier

@ -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>

Voir le fichier

@ -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>

Voir le fichier

@ -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">

Voir le fichier

@ -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">

Voir le fichier

@ -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>

Voir le fichier

@ -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>

Voir le fichier

@ -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"
} }
] ]

Voir le fichier

@ -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"
} }
] ]

Voir le fichier

@ -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é"
} }
] ]

Voir le fichier

@ -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"
}
] ]

Voir le fichier

@ -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"
} }
] ]

Voir le fichier

@ -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": "โหลดเสร็จแล้ว"
} }
] ]

Voir le fichier

@ -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)
} }

Voir le fichier

@ -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(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1)) tor, count, err = torrentService.GetTorrentsOrderBy(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
} else { } else {
tor, err = torrentService.GetTorrentsOrderByNoCount(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1)) tor, err = torrentService.GetTorrentsOrderByNoCount(&parameters, orderBy, int(search.Max), int(search.Max)*(search.Page-1))
} }
return return
}) })
return return
} }