This commit is contained in:
yourok
2018-08-29 12:33:14 +03:00
commit 0ca43a2c4d
54 changed files with 5669 additions and 0 deletions

90
src/main/main.go Normal file
View File

@@ -0,0 +1,90 @@
package main
import (
"bytes"
"errors"
"fmt"
"net/http"
"os"
"time"
"github.com/alexflint/go-arg"
"server"
"server/settings"
"server/version"
)
type args struct {
Port string `arg:"-p" help:"web server port"`
Path string `arg:"-d" help:"database path"`
Add string `arg:"-a" help:"add torrent link and exit"`
Kill bool `arg:"-k" help:"dont kill program on signal"`
}
func (args) Version() string {
return "TorrServer " + version.Version
}
var params args
func main() {
//test()
//return
//for _, g := range tmdb.GetMovieGenres("ru") {
// fmt.Println(g.Name, g.ID)
//}
//return
//movs, _ := tmdb.DiscoverShows(map[string]string{}, 1)
//js, _ := json.MarshalIndent(movs, "", " ")
//fmt.Println(string(js))
//return
arg.MustParse(&params)
if params.Path == "" {
params.Path, _ = os.Getwd()
}
if params.Port == "" {
params.Port = "8090"
}
if params.Add != "" {
add()
}
Preconfig(params.Kill)
server.Start(params.Path, params.Port)
settings.SaveSettings()
fmt.Println(server.WaitServer())
time.Sleep(time.Second * 3)
os.Exit(0)
}
func add() {
err := addRemote()
if err != nil {
fmt.Println("Error add torrent:", err)
os.Exit(-1)
}
fmt.Println("Added ok")
os.Exit(0)
}
func addRemote() error {
url := "http://localhost:" + params.Port + "/torrent/add"
fmt.Println("Add torrent link:", params.Add, "\n", url)
json := `{"Link":"` + params.Add + `"}`
resp, err := http.Post(url, "text/html; charset=utf-8", bytes.NewBufferString(json))
if err != nil {
return err
}
if resp.StatusCode != 200 {
return errors.New(resp.Status)
}
return nil
}

49
src/main/preconfig_pos.go Normal file
View File

@@ -0,0 +1,49 @@
// +build !windows
package main
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"syscall"
)
func Preconfig(kill bool) {
if kill {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGSTOP,
syscall.SIGPIPE,
syscall.SIGTERM,
syscall.SIGQUIT)
go func() {
for s := range sigc {
fmt.Println("Signal catched:", s)
fmt.Println("For stop server, close in web")
}
}()
}
//dns resover
addrs, err := net.LookupHost("www.themoviedb.org")
if len(addrs) == 0 {
fmt.Println("Check dns", addrs, err)
fn := func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
return d.DialContext(ctx, "udp", "1.1.1.1:53")
}
net.DefaultResolver = &net.Resolver{
Dial: fn,
}
addrs, err = net.LookupHost("www.themoviedb.org")
fmt.Println("Check new dns", addrs, err)
}
}

View File

@@ -0,0 +1,7 @@
// +build windows
package main
func Preconfig(kill bool) {
}

74
src/main/test.go Normal file
View File

@@ -0,0 +1,74 @@
package main
import (
"fmt"
"log"
"os"
"sync"
"time"
"server/utils"
"github.com/anacrolix/torrent"
)
func test() {
config := torrent.NewDefaultClientConfig()
config.EstablishedConnsPerTorrent = 100
config.HalfOpenConnsPerTorrent = 65
config.DisableIPv6 = true
config.NoDHT = true
client, err := torrent.NewClient(config)
if err != nil {
log.Fatal(err)
}
//Ubuntu
t, err := client.AddMagnet("magnet:?xt=urn:btih:e4be9e4db876e3e3179778b03e906297be5c8dbe&dn=ubuntu-18.04-desktop-amd64.iso&tr=http%3a%2f%2ftorrent.ubuntu.com%3a6969%2fannounce&tr=http%3a%2f%2fipv6.torrent.ubuntu.com%3a6969%2fannounce")
if err != nil {
log.Fatal(err)
}
<-t.GotInfo()
file := t.Files()[0]
reader := file.NewReader()
var wa sync.WaitGroup
wa.Add(1)
go func() {
buf := make([]byte, t.Info().PieceLength)
for {
_, err := reader.Read(buf)
if err != nil {
break
}
}
wa.Done()
}()
go func() {
cl := t.Closed()
lastTimeSpeed := time.Now()
DownloadSpeed := 0.0
BytesReadUsefulData := int64(0)
for {
select {
case <-cl:
return
default:
client.WriteStatus(os.Stdout)
st := t.Stats()
deltaDlBytes := st.BytesReadUsefulData.Int64() - BytesReadUsefulData
deltaTime := time.Since(lastTimeSpeed).Seconds()
DownloadSpeed = float64(deltaDlBytes) / deltaTime
BytesReadUsefulData = st.BytesReadUsefulData.Int64()
lastTimeSpeed = time.Now()
fmt.Println("DL speed:", utils.Format(DownloadSpeed))
}
time.Sleep(time.Second)
}
}()
wa.Wait()
}

33
src/server/Server.go Normal file
View File

@@ -0,0 +1,33 @@
package server
import (
"fmt"
"server/settings"
"server/web"
)
func Start(settingsPath, port string) {
settings.Path = settingsPath
err := settings.ReadSettings()
if err != nil {
fmt.Println("Error read settings:", err)
}
if port == "" {
port = "8090"
}
server.Start(port)
}
func WaitServer() string {
err := server.Wait()
if err != nil {
return err.Error()
}
return ""
}
func Stop() {
go server.Stop()
settings.CloseDB()
}

52
src/server/settings/DB.go Normal file
View File

@@ -0,0 +1,52 @@
package settings
import (
"fmt"
"path/filepath"
"github.com/boltdb/bolt"
)
var (
db *bolt.DB
dbInfosName = []byte("Infos")
dbTorrentsName = []byte("Torrents")
dbSettingsName = []byte("Settings")
Path string
)
func openDB() error {
if db != nil {
return nil
}
var err error
db, err = bolt.Open(filepath.Join(Path, "torrserver.db"), 0666, nil)
if err != nil {
fmt.Print(err)
return err
}
err = db.Update(func(tx *bolt.Tx) error {
_, err = tx.CreateBucketIfNotExists(dbSettingsName)
if err != nil {
return fmt.Errorf("could not create Settings bucket: %v", err)
}
_, err = tx.CreateBucketIfNotExists(dbTorrentsName)
if err != nil {
return fmt.Errorf("could not create Torrents bucket: %v", err)
}
return nil
})
if err != nil {
CloseDB()
}
return err
}
func CloseDB() {
if db != nil {
db.Close()
db = nil
}
}

View File

@@ -0,0 +1,61 @@
package settings
import (
"fmt"
"strings"
"github.com/boltdb/bolt"
)
func AddInfo(hash, info string) error {
err := openDB()
if err != nil {
return err
}
hash = strings.ToUpper(hash)
return db.Update(func(tx *bolt.Tx) error {
dbt, err := tx.CreateBucketIfNotExists([]byte(dbInfosName))
if err != nil {
return err
}
dbi, err := dbt.CreateBucketIfNotExists([]byte(hash))
if err != nil {
return err
}
err = dbi.Put([]byte("Info"), []byte(info))
if err != nil {
return fmt.Errorf("error save torrent info %v", err)
}
return nil
})
}
func GetInfo(hash string) string {
err := openDB()
if err != nil {
return "{}"
}
hash = strings.ToUpper(hash)
ret := "{}"
err = db.View(func(tx *bolt.Tx) error {
hdb := tx.Bucket(dbInfosName)
if hdb == nil {
return fmt.Errorf("could not find torrent info")
}
hdb = hdb.Bucket([]byte(hash))
if hdb != nil {
info := hdb.Get([]byte("Info"))
if info == nil {
return fmt.Errorf("error get torrent info")
}
ret = string(info)
return nil
}
return nil
})
return ret
}

View File

@@ -0,0 +1,102 @@
package settings
import (
"encoding/json"
"fmt"
"time"
"github.com/boltdb/bolt"
)
var (
sets *Settings
StartTime time.Time
)
func init() {
sets = new(Settings)
sets.CacheSize = 200 * 1024 * 1024
sets.PreloadBufferSize = 20 * 1024 * 1024
sets.ConnectionsLimit = 100
sets.RetrackersMode = 1
sets.DisableDHT = true
StartTime = time.Now()
}
type Settings struct {
CacheSize int64 // in byte, def 200 mb
PreloadBufferSize int64 // in byte, buffer for preload
RetrackersMode int //0 - don`t add, 1 - add retrackers, 2 - remove retrackers
//BT Config
DisableTCP bool
DisableUTP bool
DisableUPNP bool
DisableDHT bool
DisableUpload bool
Encryption int // 0 - Enable, 1 - disable, 2 - force
DownloadRateLimit int // in kb, 0 - inf
UploadRateLimit int // in kb, 0 - inf
ConnectionsLimit int
}
func Get() *Settings {
return sets
}
func (s *Settings) String() string {
buf, _ := json.MarshalIndent(sets, "", " ")
return string(buf)
}
func ReadSettings() error {
err := openDB()
if err != nil {
return err
}
buf := make([]byte, 0)
err = db.View(func(tx *bolt.Tx) error {
sdb := tx.Bucket(dbSettingsName)
if sdb == nil {
return fmt.Errorf("error load settings")
}
buf = sdb.Get([]byte("json"))
if buf == nil {
return fmt.Errorf("error load settings")
}
return nil
})
err = json.Unmarshal(buf, sets)
if err != nil {
return err
}
if sets.ConnectionsLimit <= 0 {
sets.ConnectionsLimit = 50
}
if sets.CacheSize <= 0 {
sets.CacheSize = 200 * 1024 * 1024
}
return nil
}
func SaveSettings() error {
err := openDB()
if err != nil {
return err
}
buf, err := json.Marshal(sets)
if err != nil {
return err
}
return db.Update(func(tx *bolt.Tx) error {
setsDB, err := tx.CreateBucketIfNotExists(dbSettingsName)
if err != nil {
return err
}
return setsDB.Put([]byte("json"), []byte(buf))
})
}

View File

@@ -0,0 +1,292 @@
package settings
import (
"encoding/binary"
"fmt"
"github.com/boltdb/bolt"
)
type Torrent struct {
Name string
Magnet string
Hash string
Size int64
Timestamp int64
Files []File
}
type File struct {
Name string
Size int64
Viewed bool
}
func SetViewed(hash, filename string) error {
err := openDB()
if err != nil {
return err
}
return db.Update(func(tx *bolt.Tx) error {
dbt := tx.Bucket(dbTorrentsName)
if dbt == nil {
return fmt.Errorf("could not find torrent")
}
hdb := dbt.Bucket([]byte(hash))
if hdb == nil {
return fmt.Errorf("could not find torrent")
}
fdb := hdb.Bucket([]byte("Files"))
if fdb == nil {
return fmt.Errorf("could not find torrent")
}
fdb = fdb.Bucket([]byte(filename))
if fdb == nil {
return fmt.Errorf("could not find torrent")
}
err = fdb.Put([]byte("Viewed"), []byte{1})
if err != nil {
return fmt.Errorf("error save torrent %v", err)
}
return nil
})
}
func SaveTorrentDB(torrent *Torrent) error {
err := openDB()
if err != nil {
return err
}
return db.Update(func(tx *bolt.Tx) error {
dbt, err := tx.CreateBucketIfNotExists(dbTorrentsName)
if err != nil {
return fmt.Errorf("could not create Torrents bucket: %v", err)
}
fmt.Println("Save torrent:", torrent.Name)
hdb, err := dbt.CreateBucketIfNotExists([]byte(torrent.Hash))
if err != nil {
return fmt.Errorf("could not create Torrent bucket: %v", err)
}
err = hdb.Put([]byte("Name"), []byte(torrent.Name))
if err != nil {
return fmt.Errorf("error save torrent: %v", err)
}
err = hdb.Put([]byte("Link"), []byte(torrent.Magnet))
if err != nil {
return fmt.Errorf("error save torrent: %v", err)
}
err = hdb.Put([]byte("Size"), i2b(torrent.Size))
if err != nil {
return fmt.Errorf("error save torrent: %v", err)
}
err = hdb.Put([]byte("Timestamp"), i2b(torrent.Timestamp))
if err != nil {
return fmt.Errorf("error save torrent: %v", err)
}
fdb, err := hdb.CreateBucketIfNotExists([]byte("Files"))
if err != nil {
return fmt.Errorf("error save torrent files: %v", err)
}
for _, f := range torrent.Files {
ffdb, err := fdb.CreateBucketIfNotExists([]byte(f.Name))
if err != nil {
return fmt.Errorf("error save torrent files: %v", err)
}
err = ffdb.Put([]byte("Size"), i2b(f.Size))
if err != nil {
return fmt.Errorf("error save torrent files: %v", err)
}
b := 0
if f.Viewed {
b = 1
}
err = ffdb.Put([]byte("Viewed"), []byte{byte(b)})
if err != nil {
return fmt.Errorf("error save torrent files: %v", err)
}
}
return nil
})
}
func RemoveTorrentDB(hash string) error {
err := openDB()
if err != nil {
return err
}
return db.Update(func(tx *bolt.Tx) error {
dbt := tx.Bucket(dbTorrentsName)
if dbt == nil {
return fmt.Errorf("could not find torrent")
}
return dbt.DeleteBucket([]byte(hash))
})
}
func LoadTorrentDB(hash string) (*Torrent, error) {
err := openDB()
if err != nil {
return nil, err
}
var torr *Torrent
err = db.View(func(tx *bolt.Tx) error {
hdb := tx.Bucket(dbTorrentsName)
if hdb == nil {
return fmt.Errorf("could not find torrent")
}
hdb = hdb.Bucket([]byte(hash))
if hdb != nil {
torr = new(Torrent)
torr.Hash = string(hash)
tmp := hdb.Get([]byte("Name"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Name = string(tmp)
tmp = hdb.Get([]byte("Link"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Magnet = string(tmp)
tmp = hdb.Get([]byte("Size"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Size = b2i(tmp)
tmp = hdb.Get([]byte("Timestamp"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Timestamp = b2i(tmp)
fdb := hdb.Bucket([]byte("Files"))
if fdb == nil {
return fmt.Errorf("error load torrent files")
}
cf := fdb.Cursor()
for fn, _ := cf.First(); fn != nil; fn, _ = cf.Next() {
file := File{Name: string(fn)}
ffdb := fdb.Bucket(fn)
if ffdb == nil {
return fmt.Errorf("error load torrent files")
}
tmp := ffdb.Get([]byte("Size"))
if tmp == nil {
return fmt.Errorf("error load torrent file")
}
file.Size = b2i(tmp)
tmp = ffdb.Get([]byte("Viewed"))
if tmp == nil {
return fmt.Errorf("error load torrent file")
}
file.Viewed = len(tmp) > 0 && tmp[0] == 1
torr.Files = append(torr.Files, file)
}
SortFiles(torr.Files)
}
return nil
})
return torr, err
}
func LoadTorrentsDB() ([]*Torrent, error) {
err := openDB()
if err != nil {
return nil, err
}
torrs := make([]*Torrent, 0)
err = db.View(func(tx *bolt.Tx) error {
tdb := tx.Bucket(dbTorrentsName)
c := tdb.Cursor()
for h, _ := c.First(); h != nil; h, _ = c.Next() {
hdb := tdb.Bucket(h)
if hdb != nil {
torr := new(Torrent)
torr.Hash = string(h)
tmp := hdb.Get([]byte("Name"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Name = string(tmp)
tmp = hdb.Get([]byte("Link"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Magnet = string(tmp)
tmp = hdb.Get([]byte("Size"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Size = b2i(tmp)
tmp = hdb.Get([]byte("Timestamp"))
if tmp == nil {
return fmt.Errorf("error load torrent")
}
torr.Timestamp = b2i(tmp)
fdb := hdb.Bucket([]byte("Files"))
if fdb == nil {
return fmt.Errorf("error load torrent files")
}
cf := fdb.Cursor()
for fn, _ := cf.First(); fn != nil; fn, _ = cf.Next() {
file := File{Name: string(fn)}
ffdb := fdb.Bucket(fn)
if ffdb == nil {
return fmt.Errorf("error load torrent files")
}
tmp := ffdb.Get([]byte("Size"))
if tmp == nil {
return fmt.Errorf("error load torrent file")
}
file.Size = b2i(tmp)
tmp = ffdb.Get([]byte("Viewed"))
if tmp == nil {
return fmt.Errorf("error load torrent file")
}
file.Viewed = len(tmp) > 0 && tmp[0] == 1
torr.Files = append(torr.Files, file)
}
SortFiles(torr.Files)
torrs = append(torrs, torr)
}
}
return nil
})
return torrs, err
}
func i2b(v int64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, uint64(v))
return b
}
func b2i(v []byte) int64 {
return int64(binary.BigEndian.Uint64(v))
}

View File

@@ -0,0 +1,88 @@
package settings
import (
"path/filepath"
"sort"
"strings"
)
var (
uFiles = map[string]interface{}{
".3g2": nil,
".3gp": nil,
".aaf": nil,
".asf": nil,
".avchd": nil,
".avi": nil,
".drc": nil,
".flv": nil,
".m2v": nil,
".m4p": nil,
".m4v": nil,
".mkv": nil,
".mng": nil,
".mov": nil,
".mp2": nil,
".mp4": nil,
".mpe": nil,
".mpeg": nil,
".mpg": nil,
".mpv": nil,
".mxf": nil,
".nsv": nil,
".ogg": nil,
".ogv": nil,
".ts": nil,
".m2ts": nil,
".mts": nil,
".qt": nil,
".rm": nil,
".rmvb": nil,
".roq": nil,
".svi": nil,
".vob": nil,
".webm": nil,
".wmv": nil,
".yuv": nil,
".aac": nil,
".aiff": nil,
".ape": nil,
".au": nil,
".flac": nil,
".gsm": nil,
".it": nil,
".m3u": nil,
".m4a": nil,
".mid": nil,
".mod": nil,
".mp3": nil,
".mpa": nil,
".pls": nil,
".ra": nil,
".s3m": nil,
".sid": nil,
".wav": nil,
".wma": nil,
".xm": nil,
}
)
func SortFiles(files []File) {
sort.Slice(files, func(i, j int) bool {
if haveUsable(files[i].Name) && !haveUsable(files[j].Name) {
return true
}
if !haveUsable(files[i].Name) && haveUsable(files[j].Name) {
return false
}
return files[i].Name < files[j].Name
})
}
func haveUsable(name string) bool {
ext := strings.ToLower(filepath.Ext(name))
_, ok := uFiles[ext]
return ok
}

182
src/server/torr/BTServer.go Normal file
View File

@@ -0,0 +1,182 @@
package torr
import (
"fmt"
"io"
"path/filepath"
"sync"
"server/settings"
"server/torr/storage"
"server/torr/storage/memcache"
"server/torr/storage/state"
"server/utils"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/iplist"
"github.com/anacrolix/torrent/metainfo"
)
type BTServer struct {
config *torrent.ClientConfig
client *torrent.Client
storage storage.Storage
torrents map[metainfo.Hash]*Torrent
mu sync.Mutex
wmu sync.Mutex
watching bool
}
func NewBTS() *BTServer {
bts := new(BTServer)
bts.torrents = make(map[metainfo.Hash]*Torrent)
return bts
}
func (bt *BTServer) Connect() error {
bt.mu.Lock()
defer bt.mu.Unlock()
var err error
bt.configure()
bt.client, err = torrent.NewClient(bt.config)
bt.torrents = make(map[metainfo.Hash]*Torrent)
return err
}
func (bt *BTServer) Disconnect() {
bt.mu.Lock()
defer bt.mu.Unlock()
if bt.client != nil {
bt.client.Close()
bt.client = nil
utils.FreeOSMemGC()
}
}
func (bt *BTServer) Reconnect() error {
bt.Disconnect()
return bt.Connect()
}
func (bt *BTServer) configure() {
bt.storage = memcache.NewStorage(settings.Get().CacheSize)
blocklist, _ := iplist.MMapPackedFile(filepath.Join(settings.Path, "blocklist"))
userAgent := "uTorrent/3.4.9"
peerID := "-UT3490-"
bt.config = torrent.NewDefaultClientConfig()
bt.config.DisableIPv6 = true
bt.config.DisableTCP = settings.Get().DisableTCP
bt.config.DisableUTP = settings.Get().DisableUTP
bt.config.NoDefaultPortForwarding = settings.Get().DisableUPNP
bt.config.NoDHT = settings.Get().DisableDHT
bt.config.NoUpload = settings.Get().DisableUpload
bt.config.EncryptionPolicy = torrent.EncryptionPolicy{
DisableEncryption: settings.Get().Encryption == 1,
ForceEncryption: settings.Get().Encryption == 2,
}
bt.config.IPBlocklist = blocklist
bt.config.DefaultStorage = bt.storage
bt.config.Bep20 = peerID
bt.config.PeerID = utils.PeerIDRandom(peerID)
bt.config.HTTPUserAgent = userAgent
bt.config.EstablishedConnsPerTorrent = settings.Get().ConnectionsLimit
bt.config.TorrentPeersHighWater = 3000
bt.config.HalfOpenConnsPerTorrent = 50
if settings.Get().DownloadRateLimit > 0 {
bt.config.DownloadRateLimiter = utils.Limit(settings.Get().DownloadRateLimit * 1024)
}
if settings.Get().UploadRateLimit > 0 {
bt.config.UploadRateLimiter = utils.Limit(settings.Get().UploadRateLimit * 1024)
}
//bt.config.Debug = true
fmt.Println("Configure client:", settings.Get())
}
func (bt *BTServer) AddTorrent(magnet metainfo.Magnet, onAdd func(*Torrent)) (*Torrent, error) {
torr, err := NewTorrent(magnet, bt)
if err != nil {
return nil, err
}
if onAdd != nil {
go func() {
if torr.GotInfo() {
onAdd(torr)
}
}()
} else {
go torr.GotInfo()
}
return torr, nil
}
func (bt *BTServer) List() []*Torrent {
bt.mu.Lock()
defer bt.mu.Unlock()
list := make([]*Torrent, 0)
for _, t := range bt.torrents {
list = append(list, t)
}
return list
}
func (bt *BTServer) GetTorrent(hash metainfo.Hash) *Torrent {
bt.mu.Lock()
defer bt.mu.Unlock()
if t, ok := bt.torrents[hash]; ok {
return t
}
return nil
}
func (bt *BTServer) RemoveTorrent(hash torrent.InfoHash) {
if torr, ok := bt.torrents[hash]; ok {
torr.Close()
}
}
func (bt *BTServer) BTState() *BTState {
bt.mu.Lock()
defer bt.mu.Unlock()
btState := new(BTState)
btState.LocalPort = bt.client.LocalPort()
btState.PeerID = fmt.Sprintf("%x", bt.client.PeerID())
btState.BannedIPs = len(bt.client.BadPeerIPs())
for _, dht := range bt.client.DhtServers() {
btState.DHTs = append(btState.DHTs, dht)
}
for _, t := range bt.torrents {
btState.Torrents = append(btState.Torrents, t)
}
return btState
}
func (bt *BTServer) CacheState(hash metainfo.Hash) *state.CacheState {
st := bt.GetTorrent(hash)
if st == nil {
return nil
}
cacheState := bt.storage.GetStats(hash)
return cacheState
}
func (bt *BTServer) WriteState(w io.Writer) {
bt.client.WriteStatus(w)
}

51
src/server/torr/Play.go Normal file
View File

@@ -0,0 +1,51 @@
package torr
import (
"fmt"
"net/http"
"time"
"server/settings"
"server/utils"
"github.com/anacrolix/missinggo/httptoo"
"github.com/anacrolix/torrent"
"github.com/labstack/echo"
)
func (bt *BTServer) View(torr *Torrent, file *torrent.File, c echo.Context) error {
go settings.SetViewed(torr.Hash().HexString(), file.Path())
reader := torr.NewReader(file, 0)
fmt.Println("Connect reader:", len(torr.readers))
c.Response().Header().Set("Connection", "close")
c.Response().Header().Set("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", torr.Hash().HexString(), file.Path())))
http.ServeContent(c.Response(), c.Request(), file.Path(), time.Time{}, reader)
fmt.Println("Disconnect reader:", len(torr.readers))
torr.CloseReader(reader)
return c.NoContent(http.StatusOK)
}
func (bt *BTServer) Play(torr *Torrent, file *torrent.File, preload int64, c echo.Context) error {
if torr.status == TorrentAdded {
if !torr.GotInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
}
if torr.status == TorrentGettingInfo {
if !torr.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
}
if torr.PreloadedBytes == 0 {
torr.Preload(file, preload)
}
redirectUrl := c.Scheme() + "://" + c.Request().Host + "/torrent/view/" + torr.Hash().HexString() + "/" + utils.CleanFName(file.Path())
return c.Redirect(http.StatusFound, redirectUrl)
//return bt.View(torr, file, c)
}

57
src/server/torr/State.go Normal file
View File

@@ -0,0 +1,57 @@
package torr
import (
"github.com/anacrolix/dht"
)
type BTState struct {
LocalPort int
PeerID string
BannedIPs int
DHTs []*dht.Server
Torrents []*Torrent
}
type TorrentStats struct {
Name string
Hash string
TorrentStatus TorrentStatus
TorrentStatusString string
LoadedSize int64
TorrentSize int64
PreloadedBytes int64
PreloadSize int64
DownloadSpeed float64
UploadSpeed float64
TotalPeers int
PendingPeers int
ActivePeers int
ConnectedSeeders int
HalfOpenPeers int
BytesWritten int64
BytesWrittenData int64
BytesRead int64
BytesReadData int64
BytesReadUsefulData int64
ChunksWritten int64
ChunksRead int64
ChunksReadUseful int64
ChunksReadWasted int64
PiecesDirtiedGood int64
PiecesDirtiedBad int64
FileStats []TorrentFileStat
}
type TorrentFileStat struct {
Id int
Path string
Length int64
}

400
src/server/torr/Torrent.go Normal file
View File

@@ -0,0 +1,400 @@
package torr
import (
"fmt"
"io"
"sort"
"sync"
"time"
"server/settings"
"server/utils"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"github.com/labstack/gommon/bytes"
)
type TorrentStatus int
func (t TorrentStatus) String() string {
switch t {
case TorrentAdded:
return "Torrent added"
case TorrentGettingInfo:
return "Torrent getting info"
case TorrentPreload:
return "Torrent preload"
case TorrentWorking:
return "Torrent working"
case TorrentClosed:
return "Torrent closed"
default:
return "Torrent unknown status"
}
}
const (
TorrentAdded = TorrentStatus(iota)
TorrentGettingInfo
TorrentPreload
TorrentWorking
TorrentClosed
)
type Torrent struct {
*torrent.Torrent
status TorrentStatus
readers map[torrent.Reader]struct{}
muTorrent sync.Mutex
muReader sync.Mutex
bt *BTServer
lastTimeSpeed time.Time
DownloadSpeed float64
UploadSpeed float64
BytesReadUsefulData int64
BytesWrittenData int64
PreloadSize int64
PreloadedBytes int64
hash metainfo.Hash
expiredTime time.Time
closed <-chan struct{}
progressTicker *time.Ticker
}
func NewTorrent(magnet metainfo.Magnet, bt *BTServer) (*Torrent, error) {
switch settings.Get().RetrackersMode {
case 1:
magnet.Trackers = append(magnet.Trackers, utils.GetDefTrackers()...)
case 2:
magnet.Trackers = nil
case 3:
magnet.Trackers = utils.GetDefTrackers()
}
goTorrent, _, err := bt.client.AddTorrentSpec(&torrent.TorrentSpec{
Trackers: [][]string{magnet.Trackers},
DisplayName: magnet.DisplayName,
InfoHash: magnet.InfoHash,
})
if err != nil {
return nil, err
}
bt.mu.Lock()
defer bt.mu.Unlock()
if tor, ok := bt.torrents[magnet.InfoHash]; ok {
return tor, nil
}
torr := new(Torrent)
torr.Torrent = goTorrent
torr.status = TorrentAdded
torr.lastTimeSpeed = time.Now()
torr.bt = bt
torr.readers = make(map[torrent.Reader]struct{})
torr.hash = magnet.InfoHash
torr.closed = goTorrent.Closed()
go torr.watch()
bt.torrents[magnet.InfoHash] = torr
return torr, nil
}
func (t *Torrent) WaitInfo() bool {
if t.Torrent == nil {
return false
}
select {
case <-t.Torrent.GotInfo():
return true
case <-t.closed:
return false
}
}
func (t *Torrent) GotInfo() bool {
if t.status == TorrentClosed {
return false
}
t.status = TorrentGettingInfo
if t.WaitInfo() {
t.status = TorrentWorking
t.expiredTime = time.Now().Add(time.Minute * 5)
return true
} else {
t.Close()
return false
}
}
func (t *Torrent) watch() {
t.progressTicker = time.NewTicker(time.Second)
defer t.progressTicker.Stop()
for {
select {
case <-t.progressTicker.C:
go t.progressEvent()
case <-t.closed:
t.Close()
return
}
}
}
func (t *Torrent) progressEvent() {
if t.expired() {
t.drop()
return
}
t.muTorrent.Lock()
if t.Torrent != nil && t.Torrent.Info() != nil {
st := t.Torrent.Stats()
deltaDlBytes := st.BytesReadUsefulData.Int64() - t.BytesReadUsefulData
deltaUpBytes := st.BytesWrittenData.Int64() - t.BytesWrittenData
deltaTime := time.Since(t.lastTimeSpeed).Seconds()
t.DownloadSpeed = float64(deltaDlBytes) / deltaTime
t.UploadSpeed = float64(deltaUpBytes) / deltaTime
t.BytesWrittenData = st.BytesWrittenData.Int64()
t.BytesReadUsefulData = st.BytesReadUsefulData.Int64()
} else {
t.DownloadSpeed = 0
t.UploadSpeed = 0
}
t.muTorrent.Unlock()
t.lastTimeSpeed = time.Now()
}
func (t *Torrent) expired() bool {
return len(t.readers) == 0 && t.expiredTime.Before(time.Now()) && (t.status == TorrentWorking || t.status == TorrentClosed)
}
func (t *Torrent) Files() []*torrent.File {
if t.Torrent != nil && t.Torrent.Info() != nil {
files := t.Torrent.Files()
return files
}
return nil
}
func (t *Torrent) Hash() metainfo.Hash {
return t.hash
}
func (t *Torrent) Status() TorrentStatus {
return t.status
}
func (t *Torrent) Length() int64 {
if t.Info() == nil {
return 0
}
return t.Torrent.Length()
}
func (t *Torrent) NewReader(file *torrent.File, readahead int64) torrent.Reader {
t.muReader.Lock()
if t.status == TorrentClosed {
return nil
}
defer t.muReader.Unlock()
reader := file.NewReader()
if readahead <= 0 {
readahead = utils.GetReadahead()
}
reader.SetReadahead(readahead)
t.readers[reader] = struct{}{}
return reader
}
func (t *Torrent) CloseReader(reader torrent.Reader) {
t.muReader.Lock()
reader.Close()
delete(t.readers, reader)
t.expiredTime = time.Now().Add(time.Second * 5)
t.muReader.Unlock()
}
func (t *Torrent) Preload(file *torrent.File, size int64) {
if size < 0 {
return
}
if t.status == TorrentGettingInfo {
t.WaitInfo()
}
t.muTorrent.Lock()
if t.status != TorrentWorking {
t.muTorrent.Unlock()
return
}
if size == 0 {
size = settings.Get().PreloadBufferSize
}
if size == 0 {
t.muTorrent.Unlock()
return
}
t.status = TorrentPreload
t.muTorrent.Unlock()
defer func() {
if t.status == TorrentPreload {
t.status = TorrentWorking
}
}()
buff5mb := int64(5 * 1024 * 1024)
startPreloadLength := size
endPreloadOffset := int64(0)
if startPreloadLength > buff5mb {
endPreloadOffset = file.Offset() + file.Length() - buff5mb
}
readerPre := t.NewReader(file, startPreloadLength)
if readerPre == nil {
return
}
defer func() {
t.CloseReader(readerPre)
t.expiredTime = time.Now().Add(time.Minute * 1)
}()
if endPreloadOffset > 0 {
readerPost := t.NewReader(file, 1)
if readerPre == nil {
return
}
readerPost.Seek(endPreloadOffset, io.SeekStart)
readerPost.SetReadahead(buff5mb)
defer func() {
t.CloseReader(readerPost)
t.expiredTime = time.Now().Add(time.Minute * 1)
}()
}
if size > file.Length() {
size = file.Length()
}
t.PreloadSize = size
var lastSize int64 = 0
errCount := 0
for t.status == TorrentPreload {
t.expiredTime = time.Now().Add(time.Minute * 1)
t.PreloadedBytes = t.Torrent.BytesCompleted()
fmt.Println("Preload:", file.Torrent().InfoHash().HexString(), bytes.Format(t.PreloadedBytes), "/", bytes.Format(t.PreloadSize), "Speed:", utils.Format(t.DownloadSpeed), "Peers:[", t.Torrent.Stats().ConnectedSeeders, "]", t.Torrent.Stats().ActivePeers, "/", t.Torrent.Stats().TotalPeers)
if t.PreloadedBytes >= t.PreloadSize {
return
}
if lastSize == t.PreloadedBytes {
errCount++
} else {
lastSize = t.PreloadedBytes
errCount = 0
}
if errCount > 120 {
return
}
time.Sleep(time.Second)
}
}
func (t *Torrent) drop() {
t.muTorrent.Lock()
if t.Torrent != nil {
t.Torrent.Drop()
t.Torrent = nil
}
t.muTorrent.Unlock()
}
func (t *Torrent) Close() {
t.status = TorrentClosed
t.bt.mu.Lock()
defer t.bt.mu.Unlock()
t.muReader.Lock()
defer t.muReader.Unlock()
for r := range t.readers {
r.Close()
}
if _, ok := t.bt.torrents[t.hash]; ok {
delete(t.bt.torrents, t.hash)
}
t.drop()
}
func (t *Torrent) Stats() TorrentStats {
t.muTorrent.Lock()
defer t.muTorrent.Unlock()
st := TorrentStats{}
st.Name = t.Name()
st.Hash = t.hash.HexString()
st.TorrentStatus = t.status
st.TorrentStatusString = t.status.String()
if t.Torrent != nil {
st.LoadedSize = t.Torrent.BytesCompleted()
st.TorrentSize = t.Length()
st.PreloadedBytes = t.PreloadedBytes
st.PreloadSize = t.PreloadSize
st.DownloadSpeed = t.DownloadSpeed
st.UploadSpeed = t.UploadSpeed
tst := t.Torrent.Stats()
st.BytesWritten = tst.BytesWritten.Int64()
st.BytesWrittenData = tst.BytesWrittenData.Int64()
st.BytesRead = tst.BytesRead.Int64()
st.BytesReadData = tst.BytesReadData.Int64()
st.BytesReadUsefulData = tst.BytesReadUsefulData.Int64()
st.ChunksWritten = tst.ChunksWritten.Int64()
st.ChunksRead = tst.ChunksRead.Int64()
st.ChunksReadUseful = tst.ChunksReadUseful.Int64()
st.ChunksReadWasted = tst.ChunksReadWasted.Int64()
st.PiecesDirtiedGood = tst.PiecesDirtiedGood.Int64()
st.PiecesDirtiedBad = tst.PiecesDirtiedBad.Int64()
st.TotalPeers = tst.TotalPeers
st.PendingPeers = tst.PendingPeers
st.ActivePeers = tst.ActivePeers
st.ConnectedSeeders = tst.ConnectedSeeders
st.HalfOpenPeers = tst.HalfOpenPeers
for i, f := range t.Files() {
st.FileStats = append(st.FileStats, TorrentFileStat{
Id: i,
Path: f.Path(),
Length: f.Length(),
})
}
sort.Slice(st.FileStats, func(i, j int) bool {
return st.FileStats[i].Path < st.FileStats[j].Path
})
}
return st
}

View File

@@ -0,0 +1,15 @@
package storage
import (
"server/torr/storage/state"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
)
type Storage interface {
storage.ClientImpl
GetStats(hash metainfo.Hash) *state.CacheState
CloseHash(hash metainfo.Hash)
}

View File

@@ -0,0 +1 @@
package filecache

View File

@@ -0,0 +1,69 @@
package filecache
import (
"path/filepath"
"sync"
"server/settings"
"server/torr/storage"
"server/torr/storage/state"
"github.com/anacrolix/missinggo/filecache"
"github.com/anacrolix/torrent/metainfo"
storage2 "github.com/anacrolix/torrent/storage"
)
type Storage struct {
storage.Storage
caches map[metainfo.Hash]*filecache.Cache
capacity int64
mu sync.Mutex
}
func NewStorage(capacity int64) storage.Storage {
stor := new(Storage)
stor.capacity = capacity
stor.caches = make(map[metainfo.Hash]*filecache.Cache)
return stor
}
func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage2.TorrentImpl, error) {
s.mu.Lock()
defer s.mu.Unlock()
path := filepath.Join(settings.Path, "cache", infoHash.String())
cache, err := filecache.NewCache(path)
if err != nil {
return nil, err
}
cache.SetCapacity(s.capacity)
s.caches[infoHash] = cache
return storage2.NewResourcePieces(cache.AsResourceProvider()).OpenTorrent(info, infoHash)
}
func (s *Storage) GetStats(hash metainfo.Hash) *state.CacheState {
s.mu.Lock()
defer s.mu.Unlock()
return nil
}
func (s *Storage) Clean() {
s.mu.Lock()
defer s.mu.Unlock()
}
func (s *Storage) CloseHash(hash metainfo.Hash) {
if s.caches == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
}
func (s *Storage) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
return nil
}

View File

@@ -0,0 +1,86 @@
package memcache
import (
"fmt"
"sync"
"server/utils"
)
type buffer struct {
pieceId int
buf []byte
used bool
}
type BufferPool struct {
buffs map[int]*buffer
frees int
size int64
mu sync.Mutex
}
func NewBufferPool(bufferLength int64, capacity int64) *BufferPool {
bp := new(BufferPool)
buffsSize := int(capacity/bufferLength) + 3
bp.frees = buffsSize
bp.size = bufferLength
return bp
}
func (b *BufferPool) mkBuffs() {
if b.buffs != nil {
return
}
b.buffs = make(map[int]*buffer, b.frees)
fmt.Println("Create", b.frees, "buffers")
for i := 0; i < b.frees; i++ {
buf := buffer{
-1,
make([]byte, b.size),
false,
}
b.buffs[i] = &buf
}
}
func (b *BufferPool) GetBuffer(p *Piece) (buff []byte, index int) {
b.mu.Lock()
defer b.mu.Unlock()
b.mkBuffs()
for id, buf := range b.buffs {
if !buf.used {
buf.used = true
buf.pieceId = p.Id
buff = buf.buf
index = id
b.frees--
//fmt.Printf("Get buffer: %v %v %v %p\n", id, p.Id, b.frees, buff)
return
}
}
fmt.Println("Create slow buffer")
return make([]byte, b.size), -1
}
func (b *BufferPool) ReleaseBuffer(index int) {
if index == -1 {
utils.FreeOSMem()
return
}
b.mu.Lock()
defer b.mu.Unlock()
b.mkBuffs()
if buff, ok := b.buffs[index]; ok {
buff.used = false
buff.pieceId = -1
b.frees++
//fmt.Println("Release buffer:", index, b.frees)
} else {
utils.FreeOSMem()
}
}
func (b *BufferPool) Len() int {
return b.frees
}

View File

@@ -0,0 +1,196 @@
package memcache
import (
"fmt"
"sort"
"sync"
"server/torr/storage/state"
"server/utils"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
)
type Cache struct {
storage.TorrentImpl
s *Storage
capacity int64
filled int64
hash metainfo.Hash
pieceLength int64
pieceCount int
piecesBuff int
muPiece sync.Mutex
muRemove sync.Mutex
isRemove bool
pieces map[int]*Piece
bufferPull *BufferPool
prcLoaded int
}
func NewCache(capacity int64, storage *Storage) *Cache {
ret := &Cache{
capacity: capacity,
filled: 0,
pieces: make(map[int]*Piece),
s: storage,
}
return ret
}
func (c *Cache) Init(info *metainfo.Info, hash metainfo.Hash) {
fmt.Println("Create cache for:", info.Name)
//Min capacity of 2 pieces length
cap := info.PieceLength * 2
if c.capacity < cap {
c.capacity = cap
}
c.pieceLength = info.PieceLength
c.pieceCount = info.NumPieces()
c.piecesBuff = int(c.capacity / c.pieceLength)
c.hash = hash
c.bufferPull = NewBufferPool(c.pieceLength, c.capacity)
for i := 0; i < c.pieceCount; i++ {
c.pieces[i] = &Piece{
Id: i,
Length: info.Piece(i).Length(),
Hash: info.Piece(i).Hash().HexString(),
cache: c,
}
}
}
func (c *Cache) Piece(m metainfo.Piece) storage.PieceImpl {
c.muPiece.Lock()
defer c.muPiece.Unlock()
if val, ok := c.pieces[m.Index()]; ok {
return val
}
return nil
}
func (c *Cache) Close() error {
c.isRemove = false
fmt.Println("Close cache for:", c.hash)
if _, ok := c.s.caches[c.hash]; ok {
delete(c.s.caches, c.hash)
}
c.pieces = nil
c.bufferPull = nil
utils.FreeOSMemGC()
return nil
}
func (c *Cache) GetState() state.CacheState {
cState := state.CacheState{}
cState.Capacity = c.capacity
cState.PiecesLength = c.pieceLength
cState.PiecesCount = c.pieceCount
cState.Hash = c.hash.HexString()
stats := make(map[int]state.ItemState, 0)
c.muPiece.Lock()
var fill int64 = 0
for _, value := range c.pieces {
stat := value.Stat()
if stat.BufferSize > 0 {
fill += stat.BufferSize
stats[stat.Id] = stat
}
}
c.filled = fill
c.muPiece.Unlock()
cState.Filled = c.filled
cState.Pieces = stats
return cState
}
func (c *Cache) cleanPieces() {
if c.isRemove {
return
}
c.muRemove.Lock()
if c.isRemove {
c.muRemove.Unlock()
return
}
c.isRemove = true
defer func() { c.isRemove = false }()
c.muRemove.Unlock()
remPieces := c.getRemPieces()
if len(remPieces) > 0 && (c.capacity < c.filled || c.bufferPull.Len() <= 1) {
remCount := int((c.filled - c.capacity) / c.pieceLength)
if remCount < 1 {
remCount = 1
}
if remCount > len(remPieces) {
remCount = len(remPieces)
}
remPieces = remPieces[:remCount]
for _, p := range remPieces {
c.removePiece(p)
}
}
}
func (c *Cache) getRemPieces() []*Piece {
pieces := make([]*Piece, 0)
fill := int64(0)
loading := 0
used := make(map[int]struct{})
for _, b := range c.bufferPull.buffs {
if b.used {
used[b.pieceId] = struct{}{}
}
}
for u := range used {
v := c.pieces[u]
if v.Size > 0 {
if v.Id > 0 {
pieces = append(pieces, v)
}
fill += v.Size
if !v.complete {
loading++
}
}
}
c.filled = fill
sort.Slice(pieces, func(i, j int) bool {
return pieces[i].accessed.Before(pieces[j].accessed)
})
c.prcLoaded = prc(c.piecesBuff-loading, c.piecesBuff)
return pieces
}
func (c *Cache) removePiece(piece *Piece) {
c.muPiece.Lock()
defer c.muPiece.Unlock()
piece.Release()
st := fmt.Sprintf("%v%% %v\t%s\t%s", c.prcLoaded, piece.Id, piece.accessed.Format("15:04:05.000"), piece.Hash)
if c.prcLoaded >= 95 {
fmt.Println("Clean memory GC:", st)
utils.FreeOSMemGC()
} else {
fmt.Println("Clean memory:", st)
utils.FreeOSMem()
}
}
func prc(val, of int) int {
return int(float64(val) * 100.0 / float64(of))
}

View File

@@ -0,0 +1,115 @@
package memcache
import (
"errors"
"io"
"sync"
"time"
"server/torr/storage/state"
"github.com/anacrolix/torrent/storage"
)
type Piece struct {
storage.PieceImpl
Id int
Hash string
Length int64
Size int64
complete bool
readed bool
accessed time.Time
buffer []byte
bufIndex int
mu sync.RWMutex
cache *Cache
}
func (p *Piece) WriteAt(b []byte, off int64) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.buffer == nil {
go p.cache.cleanPieces()
p.buffer, p.bufIndex = p.cache.bufferPull.GetBuffer(p)
if p.buffer == nil {
return 0, errors.New("Can't get buffer write")
}
}
n = copy(p.buffer[off:], b[:])
p.Size += int64(n)
p.accessed = time.Now()
return
}
func (p *Piece) ReadAt(b []byte, off int64) (n int, err error) {
p.mu.RLock()
defer p.mu.RUnlock()
size := len(b)
if size+int(off) > len(p.buffer) {
size = len(p.buffer) - int(off)
if size < 0 {
size = 0
}
}
if len(p.buffer) < int(off) || len(p.buffer) < int(off)+size {
return 0, io.ErrUnexpectedEOF
}
n = copy(b, p.buffer[int(off) : int(off)+size][:])
p.accessed = time.Now()
if int(off)+size >= len(p.buffer) {
p.readed = true
}
if int64(len(b))+off >= p.Size {
go p.cache.cleanPieces()
}
return n, nil
}
func (p *Piece) MarkComplete() error {
if len(p.buffer) == 0 {
return errors.New("piece is not complete")
}
p.complete = true
return nil
}
func (p *Piece) MarkNotComplete() error {
p.complete = false
return nil
}
func (p *Piece) Completion() storage.Completion {
return storage.Completion{
Complete: p.complete && len(p.buffer) > 0,
Ok: true,
}
}
func (p *Piece) Release() {
p.mu.Lock()
defer p.mu.Unlock()
if p.buffer != nil {
p.buffer = nil
p.cache.bufferPull.ReleaseBuffer(p.bufIndex)
p.bufIndex = -1
}
p.Size = 0
p.complete = false
}
func (p *Piece) Stat() state.ItemState {
itm := state.ItemState{
Id: p.Id,
Hash: p.Hash,
Accessed: p.accessed,
Completed: p.complete,
BufferSize: p.Size,
}
return itm
}

View File

@@ -0,0 +1,66 @@
package memcache
import (
"sync"
"server/torr/storage"
"server/torr/storage/state"
"github.com/anacrolix/torrent/metainfo"
storage2 "github.com/anacrolix/torrent/storage"
)
type Storage struct {
storage.Storage
caches map[metainfo.Hash]*Cache
capacity int64
mu sync.Mutex
}
func NewStorage(capacity int64) storage.Storage {
stor := new(Storage)
stor.capacity = capacity
stor.caches = make(map[metainfo.Hash]*Cache)
return stor
}
func (s *Storage) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (storage2.TorrentImpl, error) {
s.mu.Lock()
defer s.mu.Unlock()
ch := NewCache(s.capacity, s)
ch.Init(info, infoHash)
s.caches[infoHash] = ch
return ch, nil
}
func (s *Storage) GetStats(hash metainfo.Hash) *state.CacheState {
s.mu.Lock()
defer s.mu.Unlock()
if c, ok := s.caches[hash]; ok {
st := c.GetState()
return &st
}
return nil
}
func (s *Storage) CloseHash(hash metainfo.Hash) {
if s.caches == nil {
return
}
s.mu.Lock()
defer s.mu.Unlock()
if ch, ok := s.caches[hash]; ok {
ch.Close()
delete(s.caches, hash)
}
}
func (s *Storage) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
for _, ch := range s.caches {
ch.Close()
}
return nil
}

View File

@@ -0,0 +1,22 @@
package state
import (
"time"
)
type CacheState struct {
Hash string
Capacity int64
Filled int64
PiecesLength int64
PiecesCount int
Pieces map[int]ItemState
}
type ItemState struct {
Id int
Accessed time.Time
BufferSize int64
Completed bool
Hash string
}

View File

@@ -0,0 +1,17 @@
package utils
import (
"sync"
)
func ParallelFor(begin, end int, fn func(i int)) {
var wg sync.WaitGroup
wg.Add(end - begin)
for i := begin; i < end; i++ {
go func(i int) {
fn(i)
wg.Done()
}(i)
}
wg.Wait()
}

View File

@@ -0,0 +1,74 @@
package utils
import (
"encoding/base32"
"errors"
"math/rand"
"time"
"server/settings"
"github.com/anacrolix/torrent"
"golang.org/x/time/rate"
)
var trackers = []string{
"http://bt4.t-ru.org/ann?magnet",
"http://retracker.mgts.by:80/announce",
"http://tracker.city9x.com:2710/announce",
"http://tracker.electro-torrent.pl:80/announce",
"http://tracker.internetwarriors.net:1337/announce",
"http://tracker2.itzmx.com:6961/announce",
"udp://46.148.18.250:2710",
"udp://opentor.org:2710",
"udp://public.popcorn-tracker.org:6969/announce",
"udp://tracker.opentrackr.org:1337/announce",
"http://bt.svao-ix.ru/announce",
}
func GetDefTrackers() []string {
return trackers
}
func PeerIDRandom(peer string) string {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return peer + base32.StdEncoding.EncodeToString(randomBytes)[:20-len(peer)]
}
func GotInfo(t *torrent.Torrent, timeout int) error {
gi := t.GotInfo()
select {
case <-gi:
return nil
case <-time.Tick(time.Second * time.Duration(timeout)):
return errors.New("timeout load torrent info")
}
}
func GetReadahead() int64 {
readahead := int64(float64(settings.Get().CacheSize) * 0.33)
if readahead < 66*1024*1024 {
readahead = int64(settings.Get().CacheSize)
if readahead > 66*1024*1024 {
readahead = 66 * 1024 * 1024
}
}
return readahead
}
func Limit(i int) *rate.Limiter {
l := rate.NewLimiter(rate.Inf, 0)
if i > 0 {
b := i
if b < 16*1024 {
b = 16 * 1024
}
l = rate.NewLimiter(rate.Limit(i), b)
}
return l
}

View File

@@ -0,0 +1,33 @@
package utils
import (
"encoding/json"
"fmt"
"server/settings"
)
func AddInfo(hash, js string) {
info := settings.GetInfo(hash)
if info != "{}" {
var jsset map[string]interface{}
var err error
if err = json.Unmarshal([]byte(js), &jsset); err == nil {
var jsdb map[string]interface{}
if err = json.Unmarshal([]byte(info), &jsdb); err == nil {
for k, v := range jsset {
jsdb[k] = v
}
jsstr, err := json.Marshal(jsdb)
if err == nil {
settings.AddInfo(hash, string(jsstr))
return
}
}
}
if err != nil {
fmt.Println(err)
}
}
settings.AddInfo(hash, js)
}

78
src/server/utils/Utils.go Normal file
View File

@@ -0,0 +1,78 @@
package utils
import (
"fmt"
"regexp"
"runtime"
"runtime/debug"
"strconv"
"strings"
)
func CleanFName(file string) string {
re := regexp.MustCompile(`[ !*'();:@&=+$,/?#\[\]~"]`)
ret := re.ReplaceAllString(file, `_`)
ret = strings.Replace(ret, "__", "_", -1)
return ret
}
func FreeOSMem() {
debug.FreeOSMemory()
}
func FreeOSMemGC() {
runtime.GC()
debug.FreeOSMemory()
}
const (
_ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier
KB
MB
GB
TB
PB
EB
)
func Format(b float64) string {
multiple := ""
value := b
switch {
case b >= EB:
value /= EB
multiple = "EB"
case b >= PB:
value /= PB
multiple = "PB"
case b >= TB:
value /= TB
multiple = "TB"
case b >= GB:
value /= GB
multiple = "GB"
case b >= MB:
value /= MB
multiple = "MB"
case b >= KB:
value /= KB
multiple = "KB"
case b == 0:
return "0"
default:
return strconv.FormatInt(int64(b), 10) + "B"
}
return fmt.Sprintf("%.2f%s", value, multiple)
}
//
//func IsCyrillic(str string) bool {
// for _, r := range str {
// if unicode.Is(unicode.Cyrillic, r) {
// return true
// }
// }
// return false
//}

View File

@@ -0,0 +1,3 @@
package version
const Version = "1.1.65"

15
src/server/web/About.go Normal file
View File

@@ -0,0 +1,15 @@
package server
import (
"net/http"
"github.com/labstack/echo"
)
func initAbout(e *echo.Echo) {
e.GET("/about", aboutPage)
}
func aboutPage(c echo.Context) error {
return c.Render(http.StatusOK, "aboutPage", nil)
}

115
src/server/web/Info.go Normal file
View File

@@ -0,0 +1,115 @@
package server
import (
"fmt"
"net/http"
"sort"
"server/utils"
"github.com/anacrolix/torrent/metainfo"
"github.com/labstack/echo"
"github.com/labstack/gommon/bytes"
)
func initInfo(e *echo.Echo) {
server.GET("/cache", cachePage)
server.GET("/stat", statePage)
server.GET("/btstat", btStatePage)
}
func btStatePage(c echo.Context) error {
bts.WriteState(c.Response())
return c.NoContent(http.StatusOK)
}
func cachePage(c echo.Context) error {
return c.Render(http.StatusOK, "cachePage", nil)
}
func statePage(c echo.Context) error {
state := bts.BTState()
msg := ""
msg += fmt.Sprintf("Listen port: %d<br>\n", state.LocalPort)
msg += fmt.Sprintf("Peer ID: %+q<br>\n", state.PeerID)
msg += fmt.Sprintf("Banned IPs: %d<br>\n", state.BannedIPs)
for _, dht := range state.DHTs {
msg += fmt.Sprintf("%s DHT server at %s:<br>\n", dht.Addr().Network(), dht.Addr().String())
dhtStats := dht.Stats()
msg += fmt.Sprintf("\t&emsp;# Nodes: %d (%d good, %d banned)<br>\n", dhtStats.Nodes, dhtStats.GoodNodes, dhtStats.BadNodes)
msg += fmt.Sprintf("\t&emsp;Server ID: %x<br>\n", dht.ID())
msg += fmt.Sprintf("\t&emsp;Announces: %d<br>\n", dhtStats.SuccessfulOutboundAnnouncePeerQueries)
msg += fmt.Sprintf("\t&emsp;Outstanding transactions: %d<br>\n", dhtStats.OutstandingTransactions)
}
sort.Slice(state.Torrents, func(i, j int) bool {
return state.Torrents[i].Hash().HexString() < state.Torrents[j].Hash().HexString()
})
msg += "Torrents:<br>\n"
for _, t := range state.Torrents {
st := t.Stats()
msg += fmt.Sprintf("Name: %v<br>\n", st.Name)
msg += fmt.Sprintf("Hash: %v<br>\n", st.Hash)
msg += fmt.Sprintf("Status: %v<br>\n", st.TorrentStatus)
msg += fmt.Sprintf("Loaded Size: %v<br>\n", bytes.Format(st.LoadedSize))
msg += fmt.Sprintf("Torrent Size: %v<br>\n<br>\n", bytes.Format(st.TorrentSize))
msg += fmt.Sprintf("Preloaded Bytes: %v<br>\n", bytes.Format(st.PreloadedBytes))
msg += fmt.Sprintf("Preload Size: %v<br>\n<br>\n", bytes.Format(st.PreloadSize))
msg += fmt.Sprintf("Download Speed: %v/Sec<br>\n", utils.Format(st.DownloadSpeed))
msg += fmt.Sprintf("Upload Speed: %v/Sec<br>\n<br>\n", utils.Format(st.UploadSpeed))
msg += fmt.Sprintf("\t&emsp;TotalPeers: %v<br>\n", st.TotalPeers)
msg += fmt.Sprintf("\t&emsp;PendingPeers: %v<br>\n", st.PendingPeers)
msg += fmt.Sprintf("\t&emsp;ActivePeers: %v<br>\n", st.ActivePeers)
msg += fmt.Sprintf("\t&emsp;ConnectedSeeders: %v<br>\n", st.ConnectedSeeders)
msg += fmt.Sprintf("\t&emsp;HalfOpenPeers: %v<br>\n", st.HalfOpenPeers)
msg += fmt.Sprintf("\t&emsp;BytesWritten: %v (%v)<br>\n", st.BytesWritten, bytes.Format(st.BytesWritten))
msg += fmt.Sprintf("\t&emsp;BytesWrittenData: %v (%v)<br>\n", st.BytesWrittenData, bytes.Format(st.BytesWrittenData))
msg += fmt.Sprintf("\t&emsp;BytesRead: %v (%v)<br>\n", st.BytesRead, bytes.Format(st.BytesRead))
msg += fmt.Sprintf("\t&emsp;BytesReadData: %v (%v)<br>\n", st.BytesReadData, bytes.Format(st.BytesReadData))
msg += fmt.Sprintf("\t&emsp;BytesReadUsefulData: %v (%v)<br>\n", st.BytesReadUsefulData, bytes.Format(st.BytesReadUsefulData))
msg += fmt.Sprintf("\t&emsp;ChunksWritten: %v<br>\n", st.ChunksWritten)
msg += fmt.Sprintf("\t&emsp;ChunksRead: %v<br>\n", st.ChunksRead)
msg += fmt.Sprintf("\t&emsp;ChunksReadUseful: %v<br>\n", st.ChunksReadUseful)
msg += fmt.Sprintf("\t&emsp;ChunksReadWasted: %v<br>\n", st.ChunksReadWasted)
msg += fmt.Sprintf("\t&emsp;PiecesDirtiedGood: %v<br>\n", st.PiecesDirtiedGood)
msg += fmt.Sprintf("\t&emsp;PiecesDirtiedBad: %v<br>\n<br>\n", st.PiecesDirtiedBad)
if len(st.FileStats) > 0 {
msg += fmt.Sprintf("\t&emsp;Files:<br>\n")
for _, f := range st.FileStats {
msg += fmt.Sprintf("\t&emsp;\t&emsp;%v Size:%v<br>\n", f.Path, bytes.Format(f.Length))
}
}
hash := metainfo.NewHashFromHex(st.Hash)
cState := bts.CacheState(hash)
if cState != nil {
msg += fmt.Sprintf("CacheType:<br>\n")
msg += fmt.Sprintf("Capacity: %v<br>\n", bytes.Format(cState.Capacity))
msg += fmt.Sprintf("Filled: %v<br>\n", bytes.Format(cState.Filled))
msg += fmt.Sprintf("PiecesLength: %v<br>\n", bytes.Format(cState.PiecesLength))
msg += fmt.Sprintf("PiecesCount: %v<br>\n", cState.PiecesCount)
for _, p := range cState.Pieces {
msg += fmt.Sprintf("\t&emsp;Piece: %v\t&emsp; Access: %s\t&emsp; Buffer size: %d(%s)\t&emsp; Complete: %v\t&emsp; Hash: %s\n<br>", p.Id, p.Accessed.Format("15:04:05.000"), p.BufferSize, bytes.Format(int64(p.BufferSize)), p.Completed, p.Hash)
}
}
msg += "<hr><br><br>\n\n"
}
//msg += `
//<script>
//document.addEventListener("DOMContentLoaded", function(event) {
// setTimeout(function(){
// location.reload();
// }, 1000);
//});
//</script>
//
//`
return c.HTML(http.StatusOK, msg)
}

151
src/server/web/Server.go Normal file
View File

@@ -0,0 +1,151 @@
package server
import (
"fmt"
"log"
"net"
"net/http"
"os"
"runtime"
"time"
"server/settings"
"server/torr"
"server/version"
"server/web/mods"
"server/web/templates"
"github.com/anacrolix/sync"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
var (
server *echo.Echo
bts *torr.BTServer
mutex sync.Mutex
fnMutex sync.Mutex
err error
)
func Start(port string) {
runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println("Start web server, version:", version.Version)
bts = torr.NewBTS()
err := bts.Connect()
if err != nil {
fmt.Println("Error start torrent client:", err)
return
}
mutex.Lock()
server = echo.New()
server.HideBanner = true
server.HidePort = true
server.HTTPErrorHandler = HTTPErrorHandler
//server.Use(middleware.Logger())
server.Use(middleware.Recover())
templates.InitTemplate(server)
initTorrent(server)
initSettings(server)
initInfo(server)
initAbout(server)
mods.InitMods(server)
server.GET("/", mainPage)
server.GET("/echo", echoPage)
server.POST("/shutdown", shutdownPage)
server.GET("/js/api.js", templates.Api_JS)
go func() {
defer mutex.Unlock()
server.Listener, err = net.Listen("tcp", "0.0.0.0:"+port)
if err == nil {
err = server.Start("0.0.0.0:" + port)
}
server = nil
if err != nil {
fmt.Println("Error start web server:", err)
}
}()
}
func Stop() {
fnMutex.Lock()
defer fnMutex.Unlock()
if server != nil {
fmt.Println("Stop web server")
server.Close()
server = nil
if bts != nil {
bts.Disconnect()
bts = nil
}
}
}
func Wait() error {
mutex.Lock()
mutex.Unlock()
return err
}
func mainPage(c echo.Context) error {
return c.Render(http.StatusOK, "mainPage", nil)
}
func echoPage(c echo.Context) error {
return c.String(http.StatusOK, version.Version)
}
func shutdownPage(c echo.Context) error {
go func() {
Stop()
settings.CloseDB()
time.Sleep(time.Second * 2)
os.Exit(5)
}()
return c.NoContent(http.StatusOK)
}
func HTTPErrorHandler(err error, c echo.Context) {
var (
code = http.StatusInternalServerError
msg interface{}
)
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
msg = he.Message
if he.Internal != nil {
msg = fmt.Sprintf("%v, %v", err, he.Internal)
}
} else {
msg = http.StatusText(code)
}
if _, ok := msg.(string); ok {
msg = echo.Map{"message": msg}
}
if code != 404 && c.Request().URL.Path != "/torrent/stat" {
log.Println("Web server error:", err, c.Request().URL)
}
// Send response
if !c.Response().Committed {
if c.Request().Method == echo.HEAD { // Issue #608
err = c.NoContent(code)
} else {
err = c.JSON(code, msg)
}
if err != nil {
c.Logger().Error(err)
}
}
}

View File

@@ -0,0 +1,52 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"server/settings"
"github.com/labstack/echo"
)
func initSettings(e *echo.Echo) {
e.GET("/settings", settingsPage)
e.POST("/settings/read", settingsRead)
e.POST("/settings/write", settingsWrite)
}
func settingsPage(c echo.Context) error {
return c.Render(http.StatusOK, "settingsPage", nil)
}
func settingsRead(c echo.Context) error {
return c.JSON(http.StatusOK, settings.Get())
}
func settingsWrite(c echo.Context) error {
err := getJsSettings(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
settings.SaveSettings()
return c.JSON(http.StatusOK, "Ok")
}
func getJsSettings(c echo.Context) error {
buf, _ := ioutil.ReadAll(c.Request().Body)
decoder := json.NewDecoder(bytes.NewBuffer(buf))
err := decoder.Decode(settings.Get())
if err != nil {
if ute, ok := err.(*json.UnmarshalTypeError); ok {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset))
} else if se, ok := err.(*json.SyntaxError); ok {
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
} else {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
return nil
}

659
src/server/web/Torrent.go Normal file
View File

@@ -0,0 +1,659 @@
package server
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"server/settings"
"server/torr"
"server/utils"
"server/web/helpers"
"github.com/anacrolix/missinggo/httptoo"
"github.com/anacrolix/torrent/metainfo"
"github.com/labstack/echo"
)
func initTorrent(e *echo.Echo) {
e.POST("/torrent/add", torrentAdd)
e.POST("/torrent/upload", torrentUpload)
e.POST("/torrent/get", torrentGet)
e.POST("/torrent/rem", torrentRem)
e.POST("/torrent/list", torrentList)
e.POST("/torrent/stat", torrentStat)
e.POST("/torrent/cache", torrentCache)
e.POST("/torrent/drop", torrentDrop)
e.GET("/torrent/restart", torrentRestart)
e.GET("/torrent/playlist.m3u", torrentPlayListAll)
e.GET("/torrent/play", torrentPlay)
e.HEAD("/torrent/play", torrentPlay)
e.GET("/torrent/view/:hash/:file", torrentView)
e.HEAD("/torrent/view/:hash/:file", torrentView)
e.GET("/torrent/preload/:hash/:file", torrentPreload)
e.GET("/torrent/preload/:size/:hash/:file", torrentPreloadSize)
}
type TorrentJsonRequest struct {
Link string `json:",omitempty"`
Hash string `json:",omitempty"`
Title string `json:",omitempty"`
Info string `json:",omitempty"`
DontSave bool `json:",omitempty"`
}
type TorrentJsonResponse struct {
Name string
Magnet string
Hash string
AddTime int64
Length int64
Status torr.TorrentStatus
Playlist string
Info string
Files []TorFile `json:",omitempty"`
}
type TorFile struct {
Name string
Link string
Preload string
Size int64
Viewed bool
}
func torrentAdd(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Link == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Link must be non-empty")
}
magnet, err := helpers.GetMagnet(jreq.Link)
if err != nil {
fmt.Println("Error get magnet:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Title != "" {
magnet.DisplayName = jreq.Title
}
err = helpers.Add(bts, *magnet, !jreq.DontSave)
if err != nil {
fmt.Println("Error add torrent:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Info != "" {
go func() {
utils.AddInfo(magnet.InfoHash.HexString(), jreq.Info)
}()
}
return c.String(http.StatusOK, magnet.InfoHash.HexString())
}
func torrentUpload(c echo.Context) error {
form, err := c.MultipartForm()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
defer form.RemoveAll()
_, dontSave := form.Value["DontSave"]
var magnets []metainfo.Magnet
for _, file := range form.File {
torrFile, err := file[0].Open()
if err != nil {
return err
}
defer torrFile.Close()
mi, err := metainfo.Load(torrFile)
if err != nil {
fmt.Println("Error upload torrent", err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
info, err := mi.UnmarshalInfo()
if err != nil {
fmt.Println("Error upload torrent", err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
magnet := mi.Magnet(info.Name, mi.HashInfoBytes())
magnets = append(magnets, magnet)
}
ret := make([]string, 0)
for _, magnet := range magnets {
er := helpers.Add(bts, magnet, !dontSave)
if er != nil {
err = er
fmt.Println("Error add torrent:", magnet.String(), er)
}
ret = append(ret, magnet.InfoHash.HexString())
}
return c.JSON(http.StatusOK, ret)
}
func torrentGet(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
tor, err := settings.LoadTorrentDB(jreq.Hash)
if err != nil {
fmt.Println("Error get torrent:", jreq.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
torrStatus := torr.TorrentAdded
if tor == nil {
hash := metainfo.NewHashFromHex(jreq.Hash)
ts := bts.GetTorrent(hash)
if ts != nil {
torrStatus = ts.Status()
tor = toTorrentDB(ts)
}
}
if tor == nil {
fmt.Println("Error get: torrent not found", jreq.Hash)
return echo.NewHTTPError(http.StatusBadRequest, "Error get: torrent not found "+jreq.Hash)
}
js, err := getTorrentJS(tor)
if err != nil {
fmt.Println("Error get torrent:", tor.Hash, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
js.Status = torrStatus
return c.JSON(http.StatusOK, js)
}
func torrentRem(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
settings.RemoveTorrentDB(jreq.Hash)
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
return c.JSON(http.StatusOK, nil)
}
func torrentList(c echo.Context) error {
buf, _ := ioutil.ReadAll(c.Request().Body)
jsstr := string(buf)
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
jsreq := struct {
Request int
}{}
decoder.Decode(&jsreq)
reqType := jsreq.Request
js := make([]TorrentJsonResponse, 0)
list, _ := settings.LoadTorrentsDB()
for _, tor := range list {
jsTor, err := getTorrentJS(tor)
if err != nil {
fmt.Println("Error get torrent:", err)
} else {
js = append(js, *jsTor)
}
}
sort.Slice(js, func(i, j int) bool {
if js[i].AddTime == js[j].AddTime {
return js[i].Name < js[j].Name
}
return js[i].AddTime > js[j].AddTime
})
slist := bts.List()
find := func(tjs []TorrentJsonResponse, t *torr.Torrent) bool {
for _, j := range tjs {
if t.Hash().HexString() == j.Hash {
return true
}
}
return false
}
for _, st := range slist {
if !find(js, st) {
tdb := toTorrentDB(st)
jsTor, err := getTorrentJS(tdb)
if err != nil {
fmt.Println("Error get torrent:", err)
} else {
jsTor.Status = st.Status()
js = append(js, *jsTor)
}
}
}
if reqType == 1 {
ret := make([]TorrentJsonResponse, 0)
for _, r := range js {
if r.Status == torr.TorrentWorking || len(r.Files) > 0 {
ret = append(ret, r)
}
}
return c.JSON(http.StatusOK, ret)
} else if reqType == 2 {
ret := make([]TorrentJsonResponse, 0)
for _, r := range js {
if r.Status == torr.TorrentGettingInfo {
ret = append(ret, r)
}
}
return c.JSON(http.StatusOK, ret)
}
return c.JSON(http.StatusOK, js)
}
func torrentStat(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
hash := metainfo.NewHashFromHex(jreq.Hash)
tor := bts.GetTorrent(hash)
if tor == nil {
return echo.NewHTTPError(http.StatusNotFound)
}
stat := tor.Stats()
return c.JSON(http.StatusOK, stat)
}
func torrentCache(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
hash := metainfo.NewHashFromHex(jreq.Hash)
stat := bts.CacheState(hash)
if stat == nil {
return echo.NewHTTPError(http.StatusNotFound)
}
return c.JSON(http.StatusOK, stat)
}
func preload(hashHex, fileLink string, size int64) *echo.HTTPError {
if size > 0 {
hash := metainfo.NewHashFromHex(hashHex)
tor := bts.GetTorrent(hash)
if tor == nil {
torrDb, err := settings.LoadTorrentDB(hashHex)
if err != nil || torrDb == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
}
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
}
tor, err = bts.AddTorrent(m, nil)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
file := helpers.FindFileLink(fileLink, tor.Torrent)
if file == nil {
return echo.NewHTTPError(http.StatusNotFound, "file in torrent not found: "+fileLink)
}
tor.Preload(file, size)
}
return nil
}
func torrentPreload(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if hashHex == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
if fileLink == "" {
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
}
errHttp := preload(hashHex, fileLink, settings.Get().PreloadBufferSize)
if err != nil {
return errHttp
}
return c.NoContent(http.StatusOK)
}
func torrentPreloadSize(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
szPreload, err := url.PathUnescape(c.Param("size"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if hashHex == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
if fileLink == "" {
return echo.NewHTTPError(http.StatusBadRequest, "File link must be non-empty")
}
var size = settings.Get().PreloadBufferSize
if szPreload != "" {
sz, err := strconv.Atoi(szPreload)
if err == nil && sz > 0 {
size = int64(sz) * 1024 * 1024
}
}
errHttp := preload(hashHex, fileLink, size)
if err != nil {
return errHttp
}
//redirectUrl := c.Scheme() + "://" + c.Request().Host + filepath.Join("/torrent/view/", hashHex, fileLink)
//return c.Redirect(http.StatusFound, redirectUrl)
return c.NoContent(http.StatusOK)
}
func torrentDrop(c echo.Context) error {
jreq, err := getJsReqTorr(c)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if jreq.Hash == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Hash must be non-empty")
}
bts.RemoveTorrent(metainfo.NewHashFromHex(jreq.Hash))
return c.NoContent(http.StatusOK)
}
func torrentRestart(c echo.Context) error {
fmt.Println("Restart torrent engine")
err := bts.Reconnect()
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.String(http.StatusOK, "Ok")
}
func torrentPlayListAll(c echo.Context) error {
list, err := settings.LoadTorrentsDB()
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
m3u := helpers.MakeM3ULists(list, c.Scheme()+"://"+c.Request().Host)
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
c.Response().Header().Set("Content-Disposition", `attachment; filename="playlist.m3u"`)
http.ServeContent(c.Response(), c.Request(), "playlist.m3u", time.Now(), bytes.NewReader([]byte(m3u)))
return c.NoContent(http.StatusOK)
}
func torrentPlay(c echo.Context) error {
link := c.QueryParam("link")
if link == "" {
return echo.NewHTTPError(http.StatusBadRequest, "link should not be empty")
}
fmt.Println("Play:", c.QueryParams())
qsave := c.QueryParam("save")
qpreload := c.QueryParam("preload")
qfile := c.QueryParam("file")
qstat := c.QueryParam("stat")
mm3u := c.QueryParam("m3u")
preload := int64(0)
stat := strings.ToLower(qstat) == "true"
if qpreload != "" {
preload, _ = strconv.ParseInt(qpreload, 10, 64)
if preload > 0 {
preload *= 1024 * 1024
}
}
magnet, err := helpers.GetMagnet(link)
if err != nil {
fmt.Println("Error get magnet:", link, err)
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
tor := bts.GetTorrent(magnet.InfoHash)
if tor == nil {
tor, err = bts.AddTorrent(*magnet, nil)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
}
if stat {
return c.JSON(http.StatusOK, getTorPlayState(tor))
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
if strings.ToLower(qsave) == "true" {
if t, err := settings.LoadTorrentDB(magnet.InfoHash.HexString()); t == nil && err == nil {
torrDb := toTorrentDB(tor)
if torrDb != nil {
settings.SaveTorrentDB(torrDb)
}
}
}
if strings.ToLower(mm3u) == "true" {
mt := tor.Torrent.Metainfo()
m3u := helpers.MakeM3UPlayList(tor.Stats(), mt.Magnet(tor.Name(), tor.Hash()).String(), c.Scheme()+"://"+c.Request().Host)
c.Response().Header().Set("Content-Type", "audio/x-mpegurl")
c.Response().Header().Set("Connection", "close")
name := utils.CleanFName(tor.Name()) + ".m3u"
c.Response().Header().Set("ETag", httptoo.EncodeQuotedString(fmt.Sprintf("%s/%s", tor.Hash().HexString(), name)))
c.Response().Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
http.ServeContent(c.Response(), c.Request(), name, time.Time{}, bytes.NewReader([]byte(m3u)))
return c.NoContent(http.StatusOK)
}
files := helpers.GetPlayableFiles(tor.Stats())
if len(files) == 1 {
file := helpers.FindFile(files[0].Id, tor)
if file == nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprint("File", files[0], "not found in torrent", tor.Name()))
}
return bts.Play(tor, file, preload, c)
}
if qfile == "" && len(files) > 1 {
return c.JSON(http.StatusOK, getTorPlayState(tor))
}
fileInd, _ := strconv.Atoi(qfile)
file := helpers.FindFile(fileInd, tor)
if file == nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprint("File", files[fileInd], "not found in torrent", tor.Name()))
}
return bts.Play(tor, file, preload, c)
}
func torrentView(c echo.Context) error {
hashHex, err := url.PathUnescape(c.Param("hash"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
fileLink, err := url.PathUnescape(c.Param("file"))
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
hash := metainfo.NewHashFromHex(hashHex)
tor := bts.GetTorrent(hash)
if tor == nil {
torrDb, err := settings.LoadTorrentDB(hashHex)
if err != nil || torrDb == nil {
return echo.NewHTTPError(http.StatusBadRequest, "Torrent not found: "+hashHex)
}
m, err := metainfo.ParseMagnetURI(torrDb.Magnet)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Error parser magnet in db: "+hashHex)
}
tor, err = bts.AddTorrent(m, nil)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
if !tor.WaitInfo() {
return echo.NewHTTPError(http.StatusBadRequest, "torrent closed befor get info")
}
file := helpers.FindFileLink(fileLink, tor.Torrent)
if file == nil {
return echo.NewHTTPError(http.StatusNotFound, "File in torrent not found: "+fileLink)
}
return bts.View(tor, file, c)
}
func toTorrentDB(t *torr.Torrent) *settings.Torrent {
if t == nil {
return nil
}
tor := new(settings.Torrent)
tor.Name = t.Name()
tor.Hash = t.Hash().HexString()
tor.Timestamp = settings.StartTime.Unix()
mi := t.Torrent.Metainfo()
tor.Magnet = mi.Magnet(t.Name(), t.Torrent.InfoHash()).String()
tor.Size = t.Length()
files := t.Files()
sort.Slice(files, func(i, j int) bool {
return files[i].Path() < files[j].Path()
})
for _, f := range files {
tf := settings.File{
Name: f.Path(),
Size: f.Length(),
Viewed: false,
}
tor.Files = append(tor.Files, tf)
}
return tor
}
func getTorrentJS(tor *settings.Torrent) (*TorrentJsonResponse, error) {
js := new(TorrentJsonResponse)
mag, err := metainfo.ParseMagnetURI(tor.Magnet)
js.Name = tor.Name
if err == nil && len(tor.Name) < len(mag.DisplayName) {
js.Name = mag.DisplayName
}
js.Magnet = tor.Magnet
js.Hash = tor.Hash
js.AddTime = tor.Timestamp
js.Length = tor.Size
//fname is fake param for file name
js.Playlist = "/torrent/play?link=" + url.QueryEscape(tor.Magnet) + "&m3u=true&fname=" + utils.CleanFName(tor.Name+".m3u")
var size int64 = 0
for _, f := range tor.Files {
size += f.Size
tf := TorFile{
Name: f.Name,
Link: "/torrent/view/" + js.Hash + "/" + utils.CleanFName(f.Name),
Preload: "/torrent/preload/" + js.Hash + "/" + utils.CleanFName(f.Name),
Size: f.Size,
Viewed: f.Viewed,
}
js.Files = append(js.Files, tf)
}
if tor.Size == 0 {
js.Length = size
}
js.Info = settings.GetInfo(tor.Hash)
return js, nil
}
func getJsReqTorr(c echo.Context) (*TorrentJsonRequest, error) {
buf, _ := ioutil.ReadAll(c.Request().Body)
jsstr := string(buf)
decoder := json.NewDecoder(bytes.NewBufferString(jsstr))
js := new(TorrentJsonRequest)
err := decoder.Decode(js)
if err != nil {
if ute, ok := err.(*json.UnmarshalTypeError); ok {
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset))
} else if se, ok := err.(*json.SyntaxError); ok {
return nil, echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
} else {
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
}
return js, nil
}

View File

@@ -0,0 +1,67 @@
package server
import (
"server/torr"
"server/web/helpers"
)
type TorrentStat struct {
Name string
Hash string
TorrentStatus int
TorrentStatusString string
LoadedSize int64
TorrentSize int64
PreloadedBytes int64
PreloadSize int64
DownloadSpeed float64
UploadSpeed float64
TotalPeers int
PendingPeers int
ActivePeers int
ConnectedSeeders int
FileStats []FileStat
}
type FileStat struct {
Id int
Path string
Length int64
}
func getTorPlayState(tor *torr.Torrent) TorrentStat {
tst := tor.Stats()
ts := TorrentStat{}
ts.Name = tst.Name
ts.Hash = tst.Hash
ts.TorrentStatus = int(tst.TorrentStatus)
ts.TorrentStatusString = tst.TorrentStatusString
ts.LoadedSize = tst.LoadedSize
ts.TorrentSize = tst.TorrentSize
ts.PreloadedBytes = tst.PreloadedBytes
ts.PreloadSize = tst.PreloadSize
ts.DownloadSpeed = tst.DownloadSpeed
ts.UploadSpeed = tst.UploadSpeed
ts.TotalPeers = tst.TotalPeers
ts.PendingPeers = tst.PendingPeers
ts.ActivePeers = tst.ActivePeers
ts.ConnectedSeeders = tst.ConnectedSeeders
files := helpers.GetPlayableFiles(tst)
ts.FileStats = make([]FileStat, len(files))
for i, f := range files {
ts.FileStats[i] = FileStat{
Id: f.Id,
Path: f.Path,
Length: f.Length,
}
}
return ts
}

View File

@@ -0,0 +1,42 @@
package helpers
import "bytes"
type SeekingBuffer struct {
str string
buffer *bytes.Buffer
offset int64
size int64
}
func NewSeekingBuffer(str string) *SeekingBuffer {
return &SeekingBuffer{
str: str,
buffer: bytes.NewBufferString(str),
offset: 0,
}
}
func (fb *SeekingBuffer) Read(p []byte) (n int, err error) {
n, err = fb.buffer.Read(p)
fb.offset += int64(n)
return n, err
}
func (fb *SeekingBuffer) Seek(offset int64, whence int) (ret int64, err error) {
var newoffset int64
switch whence {
case 0:
newoffset = offset
case 1:
newoffset = fb.offset + offset
case 2:
newoffset = int64(len(fb.str)) - offset
}
if newoffset == fb.offset {
return newoffset, nil
}
fb.buffer = bytes.NewBufferString(fb.str[newoffset:])
fb.offset = newoffset
return fb.offset, nil
}

View File

@@ -0,0 +1,33 @@
package helpers
import (
"fmt"
"net/url"
"server/settings"
"server/torr"
"server/utils"
)
func MakeM3ULists(torrents []*settings.Torrent, host string) string {
m3u := "#EXTM3U\n"
for _, t := range torrents {
m3u += "#EXTINF:0," + t.Name + "\n"
m3u += host + "/torrent/play?link=" + url.QueryEscape(t.Magnet) + "&m3u=true&fname=" + utils.CleanFName(t.Name+".m3u") + "\n\n"
}
return m3u
}
func MakeM3UPlayList(tor torr.TorrentStats, magnet string, host string) string {
m3u := "#EXTM3U\n"
for _, f := range tor.FileStats {
if GetMimeType(f.Path) != "*/*" {
m3u += "#EXTINF:-1," + f.Path + "\n"
mag := url.QueryEscape(magnet)
m3u += host + "/torrent/play?link=" + mag + "&file=" + fmt.Sprint(f.Id) + "\n\n"
}
}
return m3u
}

View File

@@ -0,0 +1,86 @@
package helpers
import (
"errors"
"net/http"
"net/url"
"strings"
"time"
"github.com/anacrolix/torrent/metainfo"
)
func GetMagnet(link string) (*metainfo.Magnet, error) {
url, err := url.Parse(link)
if err != nil {
return nil, err
}
var mag *metainfo.Magnet
switch strings.ToLower(url.Scheme) {
case "magnet":
mag, err = getMag(url.String())
case "http", "https":
mag, err = getMagFromHttp(url.String())
case "":
mag, err = getMag("magnet:?xt=urn:btih:" + url.Path)
default:
mag, err = getMagFromFile(url.Path)
}
if err != nil {
return nil, err
}
return mag, nil
}
func getMag(link string) (*metainfo.Magnet, error) {
mag, err := metainfo.ParseMagnetURI(link)
return &mag, err
}
func getMagFromHttp(url string) (*metainfo.Magnet, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
client := new(http.Client)
client.Timeout = time.Duration(time.Second * 30)
req.Header.Set("User-Agent", "DWL/1.1.1 (Torrent)")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New(resp.Status)
}
minfo, err := metainfo.Load(resp.Body)
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
return &mag, nil
}
func getMagFromFile(path string) (*metainfo.Magnet, error) {
minfo, err := metainfo.LoadFromFile(path)
if err != nil {
return nil, err
}
info, err := minfo.UnmarshalInfo()
if err != nil {
return nil, err
}
mag := minfo.Magnet(info.Name, minfo.HashInfoBytes())
return &mag, nil
}

View File

@@ -0,0 +1,89 @@
package helpers
import (
"path/filepath"
"server/torr"
)
var extVideo = map[string]interface{}{
".3g2": nil,
".3gp": nil,
".aaf": nil,
".asf": nil,
".avchd": nil,
".avi": nil,
".drc": nil,
".flv": nil,
".m2ts": nil,
".ts": nil,
".m2v": nil,
".m4p": nil,
".m4v": nil,
".mkv": nil,
".mng": nil,
".mov": nil,
".mp2": nil,
".mp4": nil,
".mpe": nil,
".mpeg": nil,
".mpg": nil,
".mpv": nil,
".mxf": nil,
".nsv": nil,
".ogg": nil,
".ogv": nil,
".qt": nil,
".rm": nil,
".rmvb": nil,
".roq": nil,
".svi": nil,
".vob": nil,
".webm": nil,
".wmv": nil,
".yuv": nil,
}
var extAudio = map[string]interface{}{
".aac": nil,
".aiff": nil,
".ape": nil,
".au": nil,
".flac": nil,
".gsm": nil,
".it": nil,
".m3u": nil,
".m4a": nil,
".mid": nil,
".mod": nil,
".mp3": nil,
".mpa": nil,
".pls": nil,
".ra": nil,
".s3m": nil,
".sid": nil,
".wav": nil,
".wma": nil,
".xm": nil,
}
func GetMimeType(filename string) string {
ext := filepath.Ext(filename)
if _, ok := extVideo[ext]; ok {
return "video/*"
}
if _, ok := extAudio[ext]; ok {
return "audio/*"
}
return "*/*"
}
func GetPlayableFiles(st torr.TorrentStats) []torr.TorrentFileStat {
files := make([]torr.TorrentFileStat, 0)
for _, f := range st.FileStats {
if GetMimeType(f.Path) != "*/*" {
files = append(files, f)
}
}
return files
}

View File

@@ -0,0 +1,66 @@
package helpers
import (
"fmt"
"sort"
"time"
"server/settings"
"server/torr"
"server/utils"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
)
func Add(bts *torr.BTServer, magnet metainfo.Magnet, save bool) error {
fmt.Println("Adding torrent", magnet.String())
_, err := bts.AddTorrent(magnet, func(torr *torr.Torrent) {
torDb := new(settings.Torrent)
torDb.Name = torr.Name()
torDb.Hash = torr.Hash().HexString()
torDb.Size = torr.Length()
torDb.Magnet = magnet.String()
torDb.Timestamp = time.Now().Unix()
files := torr.Files()
sort.Slice(files, func(i, j int) bool {
return files[i].Path() < files[j].Path()
})
for _, f := range files {
ff := settings.File{
f.Path(),
f.Length(),
false,
}
torDb.Files = append(torDb.Files, ff)
}
if save {
err := settings.SaveTorrentDB(torDb)
if err != nil {
fmt.Println("Error add torrent to db:", err)
}
}
})
if err != nil {
return err
}
return nil
}
func FindFileLink(fileLink string, torr *torrent.Torrent) *torrent.File {
for _, f := range torr.Files() {
if utils.CleanFName(f.Path()) == fileLink {
return f
}
}
return nil
}
func FindFile(fileInd int, tor *torr.Torrent) *torrent.File {
files := tor.Files()
if len(files) == 0 || fileInd < 0 || fileInd >= len(files) {
return nil
}
return files[fileInd]
}

View File

@@ -0,0 +1,22 @@
package mods
import (
"github.com/labstack/echo"
)
func InitMods(e *echo.Echo) {
e.GET("/test", test)
}
func test(c echo.Context) error {
return c.HTML(200, `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="/js/api.js"></script>
</head>
<body>
</body>`)
}

View File

@@ -0,0 +1,119 @@
package templates
import "server/version"
var aboutPage = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
<script src="/js/api.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<title>About</title>
</head>
<body>
<style type="text/css">
.inline{
display:inline;
padding-left: 2%;
}
.center {
display: block;
margin-left: auto;
margin-right: auto;
}
.content {
padding: 20px;
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
<span class="navbar-brand mx-auto">
О программе
</span>
</nav>
<div class="content">
<img class="center" src='` + faviconB64 + `'/>
<h3 align="middle">TorrServer</h3>
<h4 align="middle">` + version.Version + `</h4>
<h4>Поддержка проекта:</h4>
<a class="inline" target="_blank" href="https://www.paypal.me/yourok">PayPal</a>
<br>
<a class="inline" target="_blank" href="https://money.yandex.ru/to/410013733697114/100">Yandex.Деньги</a>
<br>
<hr align="left" width="25%">
<br>
<h4>Инструкция по использованию:</h4>
<a class="inline" target="_blank" href="https://4pda.ru/forum/index.php?showtopic=896840&st=0#entry72570782">4pda.ru</a>
<p class="inline">Спасибо <b>MadAndron</b></p>
<br>
<hr align="left" width="25%">
<br>
<h4>Автор:</h4>
<b class="inline">YouROK</b>
<br>
<i class="inline">Email:</i>
<a target="_blank" class="inline" href="mailto:8yourok8@gmail.com">8YouROK8@gmail.com</a>
<br>
<i class="inline">Site: </i>
<a target="_blank" class="inline" href="https://github.com/YouROK">GitHub.com/YouROK</a>
<br>
<hr align="left" width="25%">
<br>
<h4>Спасибо всем, кто тестировал и помогал:</h4>
<b class="inline">kuzzman</b>
<br>
<i class="inline">Site: </i>
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1259550">4pda.ru</a>
<a target="_blank" class="inline" href="http://tv-box.pp.ua">tv-box.pp.ua</a>
<br>
<br>
<b class="inline">MadAndron</b>
<br>
<i class="inline">Site:</i>
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1543999">4pda.ru</a>
<br>
<br>
<b class="inline">SpAwN_LMG</b>
<br>
<i class="inline">Site:</i>
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=700929">4pda.ru</a>
<br>
<br>
<b class="inline">Zivio</b>
<br>
<i class="inline">Site:</i>
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=1195633">4pda.ru</a>
<a target="_blank" class="inline" href="http://forum.hdtv.ru/index.php?showtopic=19020">forum.hdtv.ru</a>
<br>
<br>
<b class="inline">Tw1cker Руслан Пахнев</b>
<br>
<i class="inline">Site:</i>
<a target="_blank" class="inline" href="https://4pda.ru/forum/index.php?showuser=2002724">4pda.ru</a>
<a target="_blank" class="inline" href="https://github.com/Nemiroff">GitHub.com/Nemiroff</a>
<br>
<br>
</div>
<footer class="page-footer navbar-dark bg-dark">
<span class="navbar-brand d-flex justify-content-center">
<center><h4>TorrServer ` + version.Version + `</h4></center>
</span>
</footer>
</body>
</html>
`
func (t *Template) parseAboutPage() {
parsePage(t, "aboutPage", aboutPage)
}

View File

@@ -0,0 +1,167 @@
package templates
import (
"server/version"
)
var cachePage = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
<script src="/js/api.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<title>TorrServer ` + version.Version + `</title>
</head>
<body>
<style>
.wrap {
white-space: normal;
word-wrap: break-word;
word-break: break-all;
}
.content {
margin: 1%;
}
.cache {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
}
.piece {
border: 1px dashed white;
font-size: 16px;
padding: 2px;
text-align: center;
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
<span class="navbar-brand mx-auto">
TorrServer ` + version.Version + `
</span>
</nav>
<div class="content">
<div id="torrents"></div>
<div id="cacheInfo"></div>
<div class="cache" id="cache"></div>
</div>
<footer class="page-footer navbar-dark bg-dark">
<span class="navbar-brand d-flex justify-content-center">
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
</span>
</footer>
</body>
<script>
$( document ).ready(function() {
setInterval(updateState, 1000);
});
var cacheHash = "";
var hashTorrents = "";
function updateTorrents(){
listTorrent(function(data){
var currHashTorrs = "";
for(var key in data) {
var tor = data[key];
currHashTorrs += tor.Hash;
}
if (currHashTorrs != hashTorrents){
hashTorrents = currHashTorrs;
var html = "";
html += '<div class="btn-group-vertical d-flex" role="group">';
for(var key in data) {
var tor = data[key];
html += '<button type="button" class="btn btn-secondary wrap w-100" onclick="setCache(\''+tor.Hash+'\')">'+tor.Name+'</button>';
}
html += '</div>'
$("#torrents").empty();
$(html).appendTo($("#torrents"));
}
});
}
function updateCache(){
var cache = $("#cache");
if (cacheHash!=""){
cacheTorrent(cacheHash, function(data){
var html = "";
var st = data;
html += '<span>Hash: '+st.Hash+'</span><br>';
html += '<span>Capacity: '+humanizeSize(st.Capacity)+'</span><br>';
html += '<span>Filled: '+humanizeSize(st.Filled)+'</span><br>';
html += '<span>Pieces length: '+humanizeSize(st.PiecesLength)+'</span><br>';
html += '<span>Pieces count: '+st.PiecesCount+'</span><br>';
$("#cacheInfo").html(html);
makePieces(st.PiecesCount);
for(var i = 0; i < st.PiecesCount; i++) {
var color = "silver";
var size = "";
var piece = st.Pieces[i];
if (piece){
if (piece.Completed && piece.BufferSize >= st.PiecesLength)
color = "green";
else if (piece.Completed && piece.BufferSize == 0)
color = "silver";
else if (!piece.Completed && piece.BufferSize > 0)
color = "red";
size = ' ' + humanizeSize(piece.BufferSize);
}
setPiece(i,color,size);
}
},function(){
$("#cacheInfo").empty();
cache.empty();
});
}
}
function makePieces(len){
var cache = $("#cache");
if (cache.children().length==len)
return;
var html = "";
for(var i = 0; i < len; i++) {
html += '<span class="piece" id="p'+i+'" style="background-color: silver;">'+i+'</span>';
}
cache.html(html);
}
function setPiece(i, color, size){
var piece = $("#p"+i);
piece.delay(100).css("background-color",color);
piece.text(i+''+size);
}
function contains(arr, elem) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].Id === elem) {
return true;
}
}
return false;
}
function updateState(){
updateTorrents();
updateCache();
}
function setCache(hash){
cacheHash = hash;
updateCache();
}
</script>
</html>
`
func (t *Template) parseCachePage() {
parsePage(t, "cachePage", cachePage)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,300 @@
package templates
import (
"server/version"
)
var mainPage = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
<script src="/js/api.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<title>TorrServer ` + version.Version + `</title>
</head>
<body>
<style>
.wrap {
white-space: normal;
word-wrap: break-word;
word-break: break-all;
}
.content {
margin: 1%;
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<span class="navbar-brand mx-auto">
TorrServer ` + version.Version + `
</span>
</nav>
<div class="content">
<div>
<label for="magnet">Добавить торрент: </label>
<input id="magnet" class="w-100" autocomplete="off">
</div>
<div class="btn-group d-flex" role="group">
<button id="buttonAdd" class="btn w-100" onclick="addTorr()"><i class="fas fa-plus"></i> Добавить</button>
<button id="buttonUpload" class="btn w-100"><i class="fas fa-file-upload"></i> Загрузить файл</button>
</div>
<br>
<div>
<a href="/torrent/playlist.m3u" rel="external" class="btn btn-primary w-100" role="button" ><i class="fas fa-th-list"></i> Плейлист всех торрентов</a>
</div>
<br>
<h3>Торренты: </h3>
<div id="torrents"></div>
<br>
<div class="btn-group-vertical d-flex" role="group">
<a href="/settings" rel="external" class="btn btn-primary w-100" role="button"><i class="fas fa-cog"></i> Настройки</a>
<a href="/cache" rel="external" class="btn btn-primary w-100" role="button"><i class="fas fa-info"></i> Кэш</a>
<button id="buttonShutdown" class="btn btn-primary w-100" onclick="shutdown()"><i class="fas fa-power-off"></i> Закрыть сервер</button>
</div>
<form id="uploadForm" style="display:none" action="/torrent/upload" method="post">
<input type="file" id="filesUpload" style="display:none" multiple onchange="uploadTorrent()" name="files"/>
</form>
</div>
<footer class="page-footer navbar-dark bg-dark">
<span class="navbar-brand d-flex justify-content-center">
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
</span>
</footer>
<div class="modal fade" id="preloadModal" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title wrap" id="preloadName"></h4>
</div>
<div class="modal-body">
<p id="preloadStatus"></p>
<p id="preloadBuffer"></p>
<p id="preloadPeers"></p>
<p id="preloadSpeed"></p>
<div class="progress">
<div id="preloadProgress" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div>
</div>
<br>
<a id="preloadFileLink" role="button" href="" class="btn btn-secondary wrap w-100"></a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">Закрыть</button>
</div>
</div>
</div>
</div>
<script>
function addTorr(){
var magnet = $("#magnet").val();
$("#magnet").val("");
if(magnet!=""){
addTorrent(magnet,true,
function( data ) {
loadTorrents();
},
function( data ) {
alert(data.responseJSON.message);
});
}
}
function removeTorr(hash){
if(hash!=""){
removeTorrent(hash,
function( data ) {
loadTorrents();
},
function( data ) {
alert(data.responseJSON.message);
});
}
};
function shutdown(){
shutdownServer(function( data ) {
alert(data.responseJSON.message);
});
}
$( document ).ready(function() {
watchInfo();
});
$('#buttonUpload').click(function() {
$('#filesUpload').click();
});
function uploadTorrent() {
var form = $("#uploadForm");
var formData = new FormData(document.getElementById("uploadForm"));
var data = new FormData();
$.each($('#filesUpload')[0].files, function(i, file) {
data.append('file-'+i, file);
});
$.ajax({
cache: false,
processData: false,
contentType: false,
type: form.attr('method'),
url: form.attr('action'),
data: data
}).done(function(data) {
loadTorrents();
}).fail(function(data) {
alert(data.responseJSON.message);
});
}
$('#uploadForm').submit(function(event) {
event.preventDefault();
var form = $(this);
$.ajax({
type: form.attr('method'),
url: form.attr('action'),
data: form.serialize()
}).done(function(data) {
loadTorrents();
});
});
function loadTorrents() {
listTorrent(
function( data ) {
var torrents = $("#torrents");
torrents.empty();
var html = "";
var queueInfo = [];
for(var key in data) {
var tor = data[key];
if (tor.Status==1){
queueInfo.push(tor);
continue;
}
html += tor2Html(tor);
}
if (queueInfo.length>0){
html += "<br><hr><h3>Got info: </h3>";
for(var key in queueInfo) {
var tor = queueInfo[key];
html += tor2Html(tor);
}
}
$(html).appendTo(torrents);
},
function( data ) {
alert(data.responseJSON.message);
});
}
function tor2Html(tor){
var html = '';
var name = "";
if (tor.Status==1)
name = tor.Name+' '+humanizeSize(tor.Length)+' '+tor.Hash;
else
name = tor.Name+' '+humanizeSize(tor.Length);
html += '<div class="btn-group d-flex" role="group">';
html += ' <button type="button" class="btn btn-secondary wrap w-100" data-toggle="collapse" data-target="#info_'+tor.Hash+'">'+name+'</button>';
if (tor.Status!=1)
html += ' <a role="button" class="btn btn-secondary" href="'+tor.Playlist+'"><i class="fas fa-th-list"></i> Плейлист</a>';
else
html += ' <button type="button" class="btn btn-secondary" onclick="showPreload(\'\', \''+ tor.Hash +'\');"><i class="fas fa-info"></i></a>';
html += ' <button type="button" class="btn btn-secondary" onclick="removeTorrent(\''+tor.Hash+'\');"><i class="fas fa-trash-alt"></i> Удалить</button>';
html += '</div>';
html += '<div class="collapse" id="info_'+tor.Hash+'">';
for(var i in tor.Files){
var file = tor.Files[i];
var ico = "";
if (file.Viewed)
ico = '<i class="far fa-eye"></i> ';
html += ' <div class="btn-group d-flex" role="group">';
html += ' <a role="button" href="'+file.Link+'" class="btn btn-secondary wrap w-100">'+ico+file.Name+" "+humanizeSize(file.Size)+'</a>';
html += ' <button type="button" class="btn btn-secondary" onclick="showPreload(\''+ file.Preload +'\', \''+ file.Link +'\', \''+ tor.Hash +'\');"><i class="fas fa-info"></i></button>';
html += ' </div>';
}
html += '<hr></div>';
return html;
}
function watchInfo(){
var lastTorrentCount = 0;
var lastGettingInfo = 0;
setInterval(function() {
listTorrent(
function( data ) {
var gettingInfo = 0;
for(var key in data) {
var tor = data[key];
if (tor.Status==1)
gettingInfo++;
}
if (lastTorrentCount!=data.length || gettingInfo!=lastGettingInfo){
loadTorrents();
lastTorrentCount = data.length;
lastGettingInfo = gettingInfo;
}
});
}, 1000);
}
function showPreload(preloadlink, fileLink, hash){
$('#preloadFileLink').hide(0);
$('#preloadFileLink').attr("href","");
$('#preloadProgress').width('100%');
if (preloadlink!='')
preloadTorrent(preloadlink);
var ptimer = setInterval(function() {
statTorrent(hash,function(data){
if (data!=null){
$('#preloadStatus').text("Status: " + data.TorrentStatusString);
$('#preloadName').text(data.Name);
$('#preloadPeers').text("Peers: [" + data.ConnectedSeeders + "] " + data.ActivePeers + " / " + data.TotalPeers);
if (data.DownloadSpeed>0)
$('#preloadSpeed').text("Speed: "+ humanizeSize(data.DownloadSpeed) + "/Sec");
else
$('#preloadSpeed').text("Speed:");
if (data.PreloadSize>0 && data.PreloadedBytes<data.PreloadSize){
var prc = data.PreloadedBytes * 100 / data.PreloadSize;
if (prc>100) prc = 100;
$('#preloadProgress').width(prc+'%');
$('#preloadBuffer').text("Loaded: " + humanizeSize(data.PreloadedBytes) + " / " + humanizeSize(data.PreloadSize)+" "+prc+"%");
}else{
$('#preloadProgress').width('100%');
$('#preloadBuffer').text("Loaded: " + humanizeSize(data.BytesReadUsefulData));
$('#preloadProgress').width('100%');
if (data.BytesReadUsefulData>0 && fileLink && !$('#preloadFileLink').attr("href")){
$('#preloadFileLink').text(data.Name);
$('#preloadFileLink').attr("href", fileLink);
$('#preloadFileLink').show();
}
}
}
},function(){
$('#preloadModal').modal('hide');
})
}, 500);
$('#preloadModal').modal('show');
$("#preloadModal").on('hidden.bs.modal', function () {
clearInterval(ptimer);
});
}
</script>
</body>
</html>
`
func (t *Template) parseMainPage() {
parsePage(t, "mainPage", mainPage)
}

View File

@@ -0,0 +1,200 @@
package templates
import "server/version"
var settingsPage = `
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="` + faviconB64 + `" rel="icon" type="image/x-icon">
<script src="/js/api.js"></script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.0/css/all.css" integrity="sha384-lKuwvrZot6UHsBSfcMvOkWwlCMgc0TaWr+30HWe3a4ltaBwTZhyTEggF5tJv8tbt" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">
<script src="http://code.jquery.com/jquery-1.11.3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js" integrity="sha384-smHYKdLADwkXOn1EmN1qk/HfnUcbVRZyYmZ4qpPea6sjB/pTJ0euyQp0Mk8ck+5T" crossorigin="anonymous"></script>
<title>TorrServer ` + version.Version + ` Settings</title>
</head>
<body>
<style>
.wrap {
white-space: normal;
word-wrap: break-word;
word-break: break-all;
}
.content {
margin: 1%;
}
</style>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<a class="btn navbar-btn pull-left" href="/"><i class="fas fa-arrow-left"></i></a>
<span class="navbar-brand mx-auto">
TorrServer ` + version.Version + `
</span>
</nav>
<div class="content">
<form id="settings">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Размер кэша</div>
</div>
<input id="CacheSize" class="form-control" type="number" autocomplete="off">
</div>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Размер буфера предзагрузки</div>
</div>
<input id="PreloadBufferSize" class="form-control" type="number" autocomplete="off">
</div>
<small class="form-text text-muted">Размеры кэша и буфера указываются в мегабайтах</small>
<br>
<div class="form-check">
<input id="DisableTCP" class="form-check-input" type="checkbox" autocomplete="off">
<label for="DisableTCP">Отключить TCP</label>
</div>
<div class="form-check">
<input id="DisableUTP" class="form-check-input" type="checkbox" autocomplete="off">
<label for="DisableUTP">Отключить UTP</label>
</div>
<div class="form-check">
<input id="DisableUPNP" class="form-check-input" type="checkbox" autocomplete="off">
<label for="DisableUPNP">Отключить UPNP</label>
</div>
<div class="form-check">
<input id="DisableDHT" class="form-check-input" type="checkbox" autocomplete="off">
<label for="DisableDHT">Отключить DHT</label>
</div>
<div class="form-check">
<input id="DisableUpload" class="form-check-input" type="checkbox" autocomplete="off">
<label for="DisableUpload">Отключить Отдачу</label>
</div>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Шифрование</div>
</div>
<select id="Encryption" class="form-control">
<option value="0">По умолчанию</option>
<option value="1">Отключить</option>
<option value="2">Принудительно</option>
</select>
</div>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Количество соединений</div>
</div>
<input id="ConnectionsLimit" class="form-control" type="number" autocomplete="off">
</div>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Ограничение загрузки</div>
</div>
<input id="DownloadRateLimit" class="form-control" type="number" autocomplete="off">
</div>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Ограничение отдачи</div>
</div>
<input id="UploadRateLimit" class="form-control" type="number" autocomplete="off">
</div>
<small class="form-text text-muted">Ограничение устанавливается в Килобайтах, 0 - не ограничивать</small>
<br>
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">Ретрекеры</div>
</div>
<select id="RetrackersMode" class="form-control">
<option value="0">Оставить как есть</option>
<option value="1">Добавить</option>
<option value="2">Удалить</option>
</select>
</div>
</form>
<br>
<div class="btn-group d-flex" role="group">
<button id="buttonSave" class="btn btn-primary w-100" data-icon="check" onclick="saveSettings()"><i class="far fa-save"></i> Сохранить</button>
<button id="buttonRefresh" class="btn btn-primary w-100" data-icon="refresh" onclick="refreshSettings()"><i class="fas fa-sync-alt"></i> Получить с сервера</button>
</div>
</div>
<footer class="page-footer navbar-dark bg-dark">
<span class="navbar-brand d-flex justify-content-center">
<a rel="external" style="text-decoration: none;" href="/about">Описание</a>
</span>
</footer>
<script>
function saveSettings() {
var data = {};
data.CacheSize = Number($('#CacheSize').val())*(1024*1024);
data.PreloadBufferSize = Number($('#PreloadBufferSize').val())*(1024*1024);
data.DisableTCP = $('#DisableTCP').prop('checked');
data.DisableUTP = $('#DisableUTP').prop('checked');
data.DisableUPNP = $('#DisableUPNP').prop('checked');
data.DisableDHT = $('#DisableDHT').prop('checked');
data.DisableUpload = $('#DisableUpload').prop('checked');
data.Encryption = Number($('#Encryption').val());
data.ConnectionsLimit = Number($('#ConnectionsLimit').val());
data.DownloadRateLimit = Number($('#DownloadRateLimit').val());
data.UploadRateLimit = Number($('#UploadRateLimit').val());
data.RetrackersMode = Number($('#RetrackersMode').val());
$.post("/settings/write", JSON.stringify(data))
.done(function(data) {
restartService();
alert(data);
})
.fail(function(data) {
alert(data.responseJSON.message);
});
}
function refreshSettings() {
$.post("/settings/read")
.done(function(data) {
$('#CacheSize').val(data.CacheSize/(1024*1024));
$('#PreloadBufferSize').val(data.PreloadBufferSize/(1024*1024));
$('#DisableTCP').prop('checked', data.DisableTCP);
$('#DisableUTP').prop('checked', data.DisableUTP);
$('#DisableUPNP').prop('checked', data.DisableUPNP);
$('#DisableDHT').prop('checked', data.DisableDHT);
$('#DisableUpload').prop('checked', data.DisableUpload);
$('#Encryption').val(data.Encryption);
$('#ConnectionsLimit').val(data.ConnectionsLimit);
$('#DownloadRateLimit').val(data.DownloadRateLimit);
$('#UploadRateLimit').val(data.UploadRateLimit);
$('#RetrackersMode').val(data.RetrackersMode);
});
};
$(document).ready(function() {
refreshSettings();
});
$(document).on("wheel", "input[type=number]", function (e) {
$(this).blur();
});
</script>
</body>
</html>
`
func (t *Template) parseSettingsPage() {
parsePage(t, "settingsPage", settingsPage)
}

View File

@@ -0,0 +1,45 @@
package templates
import (
"html/template"
"io"
"github.com/labstack/echo"
)
type Template struct {
templates *template.Template
}
func InitTemplate(e *echo.Echo) {
temp := new(Template)
temp.parseMainPage()
temp.parseSettingsPage()
temp.parseAboutPage()
temp.parseCachePage()
e.Renderer = temp
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func parsePage(temp *Template, name, page string) error {
s := page
var tmpl *template.Template
if temp.templates == nil {
temp.templates = template.New(name)
}
if name == temp.templates.Name() {
tmpl = temp.templates
} else {
tmpl = temp.templates.New(name)
}
_, err := tmpl.Parse(s)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,133 @@
package templates
import (
"net/http"
"server/settings"
"server/web/helpers"
"github.com/labstack/echo"
)
var apijs = `
function addTorrent(link, save, info, done, fail){
var reqJson = JSON.stringify({ Link: link, Info: info, DontSave: !save});
$.post('/torrent/add',reqJson)
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function getTorrent(hash, done, fail){
var reqJson = JSON.stringify({ Hash: hash});
$.post('/torrent/get',reqJson)
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function removeTorrent(hash, done, fail){
var reqJson = JSON.stringify({ Hash: hash});
$.post('/torrent/rem',reqJson)
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function statTorrent(hash, done, fail){
var reqJson = JSON.stringify({ Hash: hash});
$.post('/torrent/stat',reqJson)
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function cacheTorrent(hash, done, fail){
var reqJson = JSON.stringify({ Hash: hash});
$.post('/torrent/cache',reqJson)
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function listTorrent(done, fail){
$.post('/torrent/list')
.done(function( data ) {
if (done)
done(data);
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function restartService(done, fail){
$.get('/torrent/restart')
.done(function( data ) {
if (done)
done();
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function preloadTorrent(preloadLink, done, fail){
$.get(preloadLink)
.done(function( data ) {
if (done)
done();
})
.fail(function( data ) {
if (fail)
fail(data);
});
}
function shutdownServer(fail){
$.post('/shutdown')
.fail(function( data ) {
if (fail)
fail(data);
});
}
function humanizeSize(size) {
if (typeof size == 'undefined' || size == 0)
return "";
var i = Math.floor( Math.log(size) / Math.log(1024) );
return ( size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
`
func Api_JS(c echo.Context) error {
http.ServeContent(c.Response(), c.Request(), "api.js", settings.StartTime, helpers.NewSeekingBuffer(apijs))
return c.NoContent(http.StatusOK)
}