Albirew/nyaa-pantsu
Albirew
/
nyaa-pantsu
Archivé
1
0
Bifurcation 0

New upload API (#897)

* New upload API (WIP)

* update

* Last fix

* Adding changes from update.go to api.go

* fix bracket

* fix import
Cette révision appartient à :
akuma06 2017-06-05 15:19:25 +02:00 révisé par alucard0134
Parent cdd84db36e
révision b61fe55f82
9 fichiers modifiés avec 487 ajouts et 572 suppressions

Voir le fichier

@ -2,6 +2,7 @@ package router
import (
"encoding/json"
"errors"
"fmt"
"html"
"net/http"
@ -16,6 +17,7 @@ import (
"github.com/NyaaPantsu/nyaa/service/api"
"github.com/NyaaPantsu/nyaa/service/torrent"
"github.com/NyaaPantsu/nyaa/service/upload"
"github.com/NyaaPantsu/nyaa/service/user"
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/log"
"github.com/NyaaPantsu/nyaa/util/search"
@ -135,10 +137,14 @@ func APIViewHeadHandler(w http.ResponseWriter, r *http.Request) {
// APIUploadHandler : Controller for uploading a torrent with api
func APIUploadHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user := model.User{}
db.ORM.Where("api_token = ?", token).First(&user) //i don't like this
user, _, _, err := userService.RetrieveUserByAPIToken(token)
defer r.Body.Close()
if err != nil {
http.Error(w, "Error API token doesn't exist", http.StatusBadRequest)
return
}
if !uploadService.IsUploadEnabled(user) {
http.Error(w, "Error uploads are disabled", http.StatusBadRequest)
return
@ -150,139 +156,124 @@ func APIUploadHandler(w http.ResponseWriter, r *http.Request) {
}
upload := apiService.TorrentRequest{}
var filesize int64
contentType := r.Header.Get("Content-Type")
if contentType == "application/json" {
d := json.NewDecoder(r.Body)
if err := d.Decode(&upload); err != nil {
decodeError := fmt.Errorf("Unable to decode upload data: %s", err).Error()
http.Error(w, decodeError, http.StatusInternalServerError)
return
}
err, code := upload.ValidateUpload()
if err != nil {
http.Error(w, err.Error(), code)
return
}
} else if strings.HasPrefix(contentType, "multipart/form-data") {
upload.Name = r.FormValue("name")
upload.Category, _ = strconv.Atoi(r.FormValue("category"))
upload.SubCategory, _ = strconv.Atoi(r.FormValue("sub_category"))
upload.Description = r.FormValue("description")
upload.Remake, _ = strconv.ParseBool(r.FormValue("remake"))
upload.WebsiteLink = r.FormValue("website_link")
var err error
var code int
filesize, err, code = upload.ValidateMultipartUpload(r)
if err != nil {
http.Error(w, err.Error(), code)
return
}
} else {
if contentType != "application/json" && !strings.HasPrefix(contentType, "multipart/form-data") && r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
// TODO What should we do here ? upload is empty so we shouldn't
// create a torrent from it
err := fmt.Errorf("Please provide either of Content-Type: application/json header or multipart/form-data").Error()
http.Error(w, err, http.StatusInternalServerError)
http.Error(w, errors.New("Please provide either of Content-Type: application/json header or multipart/form-data").Error(), http.StatusInternalServerError)
return
}
// As long as the right content-type is sent, formValue is smart enough to parse it
err = upload.ExtractInfo(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
var sameTorrents int
db.ORM.Model(&model.Torrent{}).Where("torrent_hash = ?", upload.Hash).Count(&sameTorrents)
status := model.TorrentStatusNormal
if upload.Remake { // overrides trusted
status = model.TorrentStatusRemake
} else if user.IsTrusted() {
status = model.TorrentStatusTrusted
}
if sameTorrents == 0 {
torrent := model.Torrent{
Name: upload.Name,
Category: upload.Category,
SubCategory: upload.SubCategory,
Status: model.TorrentStatusNormal,
Hash: upload.Hash,
Date: time.Now(),
Filesize: filesize,
Description: upload.Description,
UploaderID: user.ID,
Uploader: &user,
WebsiteLink: upload.WebsiteLink,
err = torrentService.ExistOrDelete(upload.Infohash, user)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
torrent := model.Torrent{
Name: upload.Name,
Category: upload.CategoryID,
SubCategory: upload.SubCategoryID,
Status: status,
Hidden: upload.Hidden,
Hash: upload.Infohash,
Date: time.Now(),
Filesize: upload.Filesize,
Description: upload.Description,
WebsiteLink: upload.WebsiteLink,
UploaderID: user.ID}
torrent.ParseTrackers(upload.Trackers)
db.ORM.Create(&torrent)
if db.ElasticSearchClient != nil {
err := torrent.AddToESIndex(db.ElasticSearchClient)
if err == nil {
log.Infof("Successfully added torrent to ES index.")
} else {
log.Errorf("Unable to add torrent to ES index: %s", err)
}
if upload.Remake {
torrent.Status = model.TorrentStatusRemake
} else if user.IsTrusted() {
torrent.Status = model.TorrentStatusTrusted
}
db.ORM.Create(&torrent)
if db.ElasticSearchClient != nil {
err := torrent.AddToESIndex(db.ElasticSearchClient)
if err == nil {
log.Infof("Successfully added torrent to ES index.")
} else {
log.Errorf("Unable to add torrent to ES index: %s", err)
}
}
/*if err != nil {
util.SendError(w, err, 500)
return
}*/
fmt.Printf("%+v\n", torrent)
} else {
http.Error(w, "torrent already exists", http.StatusBadRequest)
return
log.Errorf("Unable to create elasticsearch client: %s", err)
}
torrentService.NewTorrentEvent(Router, user, &torrent)
// add filelist to files db, if we have one
if len(upload.FileList) > 0 {
for _, uploadedFile := range upload.FileList {
file := model.File{TorrentID: torrent.ID, Filesize: upload.Filesize}
err := file.SetPath(uploadedFile.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
db.ORM.Create(&file)
}
}
/*if err != nil {
util.SendError(w, err, 500)
return
}*/
fmt.Printf("%+v\n", torrent)
}
// APIUpdateHandler : Controller for updating a torrent with api
// FIXME Impossible to update a torrent uploaded by user 0
func APIUpdateHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user := model.User{}
db.ORM.Where("api_token = ?", token).First(&user) //i don't like this
user, _, _, err := userService.RetrieveUserByAPIToken(token)
defer r.Body.Close()
if err != nil {
http.Error(w, "Error API token doesn't exist", http.StatusBadRequest)
return
}
if !uploadService.IsUploadEnabled(user) {
http.Error(w, "Error uploads are disabled", http.StatusBadRequest)
return
}
contentType := r.Header.Get("Content-Type")
if contentType == "application/json" {
if user.ID == 0 {
http.Error(w, apiService.ErrAPIKey.Error(), http.StatusForbidden)
return
}
update := apiService.UpdateRequest{}
d := json.NewDecoder(r.Body)
if err := d.Decode(&update); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id := update.ID
torrent := model.Torrent{}
db.ORM.Where("torrent_id = ?", id).First(&torrent)
if torrent.ID == 0 {
http.Error(w, apiService.ErrTorrentID.Error(), http.StatusBadRequest)
return
}
if torrent.UploaderID != 0 && torrent.UploaderID != user.ID { //&& user.Status != mod
http.Error(w, apiService.ErrRights.Error(), http.StatusForbidden)
return
}
err, code := update.Update.ValidateUpdate()
if err != nil {
http.Error(w, err.Error(), code)
return
}
update.UpdateTorrent(&torrent)
torrentService.UpdateTorrent(torrent)
if user.ID == 0 {
http.Error(w, apiService.ErrAPIKey.Error(), http.StatusForbidden)
return
}
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" && !strings.HasPrefix(contentType, "multipart/form-data") && r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
// TODO What should we do here ? upload is empty so we shouldn't
// create a torrent from it
http.Error(w, errors.New("Please provide either of Content-Type: application/json header or multipart/form-data").Error(), http.StatusInternalServerError)
return
}
update := apiService.UpdateRequest{}
err = update.Update.ExtractEditInfo(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
torrent := model.Torrent{}
db.ORM.Where("torrent_id = ?", r.FormValue("id")).First(&torrent)
if torrent.ID == 0 {
http.Error(w, apiService.ErrTorrentID.Error(), http.StatusBadRequest)
return
}
if torrent.UploaderID != 0 && torrent.UploaderID != user.ID { //&& user.Status != mod
http.Error(w, apiService.ErrRights.Error(), http.StatusForbidden)
return
}
update.UpdateTorrent(&torrent, user)
torrentService.UpdateTorrent(torrent)
}
// APISearchHandler : Controller for searching with api

Voir le fichier

@ -12,6 +12,7 @@ import (
"github.com/NyaaPantsu/nyaa/db"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/api"
"github.com/NyaaPantsu/nyaa/service/comment"
"github.com/NyaaPantsu/nyaa/service/report"
"github.com/NyaaPantsu/nyaa/service/torrent"
@ -271,7 +272,7 @@ func TorrentEditModPanel(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
torrentJSON := torrent.ToJSON()
uploadForm := newUploadForm()
uploadForm := apiService.NewTorrentRequest()
uploadForm.Name = torrentJSON.Name
uploadForm.Category = torrentJSON.Category + "_" + torrentJSON.SubCategory
uploadForm.Status = torrentJSON.Status
@ -285,7 +286,7 @@ func TorrentEditModPanel(w http.ResponseWriter, r *http.Request) {
// TorrentPostEditModPanel : Controller for editing a torrent after POST request
func TorrentPostEditModPanel(w http.ResponseWriter, r *http.Request) {
var uploadForm uploadForm
var uploadForm apiService.TorrentRequest
defer r.Body.Close()
id := r.URL.Query().Get("id")
messages := msg.GetMessages(r)

Voir le fichier

@ -1,313 +0,0 @@
package router
import (
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"github.com/NyaaPantsu/nyaa/cache"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/service/upload"
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/categories"
"github.com/NyaaPantsu/nyaa/util/metainfo"
"github.com/zeebo/bencode"
)
// Use this, because we seem to avoid using models, and we would need
// the torrent ID to create the File in the DB
type uploadedFile struct {
Path []string
Filesize int64
}
// uploadForm serializing HTTP form for torrent upload
type uploadForm struct {
Name string
Magnet string
Category string
Remake bool
Description string
Status int
Hidden bool
CaptchaID string
WebsiteLink string
Infohash string
CategoryID int
SubCategoryID int
Filesize int64
Filepath string
FileList []uploadedFile
Trackers []string
}
// TODO: these should be in another package (?)
// form names
const uploadFormName = "name"
const uploadFormTorrent = "torrent"
const uploadFormMagnet = "magnet"
const uploadFormCategory = "c"
const uploadFormRemake = "remake"
const uploadFormDescription = "desc"
const uploadFormWebsiteLink = "website_link"
const uploadFormStatus = "status"
const uploadFormHidden = "hidden"
// error indicating that you can't send both a magnet link and torrent
var errTorrentPlusMagnet = errors.New("Upload either a torrent file or magnet link, not both")
// error indicating a torrent is private
var errPrivateTorrent = errors.New("Torrent is private")
// error indicating a problem with its trackers
var errTrackerProblem = errors.New("Torrent does not have any (working) trackers: " + config.Conf.WebAddress.Nyaa + "/faq#trackers")
// error indicating a torrent's name is invalid
var errInvalidTorrentName = errors.New("Torrent name is invalid")
// error indicating a torrent's description is invalid
var errInvalidTorrentDescription = errors.New("Torrent description is invalid")
// error indicating a torrent's website link is invalid
var errInvalidWebsiteLink = errors.New("Website url or IRC link is invalid")
// error indicating a torrent's category is invalid
var errInvalidTorrentCategory = errors.New("Torrent category is invalid")
// var p = bluemonday.UGCPolicy()
/**
uploadForm.ExtractInfo takes an http request and computes all fields for this form
*/
func (f *uploadForm) ExtractInfo(r *http.Request) error {
f.Name = r.FormValue(uploadFormName)
f.Category = r.FormValue(uploadFormCategory)
f.Description = r.FormValue(uploadFormDescription)
f.WebsiteLink = r.FormValue(uploadFormWebsiteLink)
f.Status, _ = strconv.Atoi(r.FormValue(uploadFormStatus))
f.Magnet = r.FormValue(uploadFormMagnet)
f.Remake = r.FormValue(uploadFormRemake) == "on"
f.Hidden = r.FormValue(uploadFormHidden) == "on"
// trim whitespace
f.Name = strings.TrimSpace(f.Name)
f.Description = util.Sanitize(strings.TrimSpace(f.Description), "default")
f.WebsiteLink = strings.TrimSpace(f.WebsiteLink)
f.Magnet = strings.TrimSpace(f.Magnet)
cache.Impl.ClearAll()
defer r.Body.Close()
if len(f.Description) > 500 {
return errInvalidTorrentDescription
}
catsSplit := strings.Split(f.Category, "_")
// need this to prevent out of index panics
if len(catsSplit) == 2 {
CatID, err := strconv.Atoi(catsSplit[0])
if err != nil {
return errInvalidTorrentCategory
}
SubCatID, err := strconv.Atoi(catsSplit[1])
if err != nil {
return errInvalidTorrentCategory
}
if !categories.CategoryExists(f.Category) {
return errInvalidTorrentCategory
}
f.CategoryID = CatID
f.SubCategoryID = SubCatID
} else {
return errInvalidTorrentCategory
}
if f.WebsiteLink != "" {
// WebsiteLink
urlRegexp, _ := regexp.Compile(`^(https?:\/\/|ircs?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$`)
if !urlRegexp.MatchString(f.WebsiteLink) {
return errInvalidWebsiteLink
}
}
// first: parse torrent file (if any) to fill missing information
tfile, _, err := r.FormFile(uploadFormTorrent)
if err == nil {
var torrent metainfo.TorrentFile
// decode torrent
_, seekErr := tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return seekErr
}
err = bencode.NewDecoder(tfile).Decode(&torrent)
if err != nil {
return metainfo.ErrInvalidTorrentFile
}
// check a few things
if torrent.IsPrivate() {
return errPrivateTorrent
}
trackers := torrent.GetAllAnnounceURLS()
f.Trackers = uploadService.CheckTrackers(trackers)
if len(f.Trackers) == 0 {
return errTrackerProblem
}
// Name
if len(f.Name) == 0 {
f.Name = torrent.TorrentName()
}
// Magnet link: if a file is provided it should be empty
if len(f.Magnet) != 0 {
return errTorrentPlusMagnet
}
_, seekErr = tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return seekErr
}
infohash, err := metainfo.DecodeInfohash(tfile)
if err != nil {
return metainfo.ErrInvalidTorrentFile
}
f.Infohash = infohash
f.Magnet = util.InfoHashToMagnet(infohash, f.Name, trackers...)
// extract filesize
f.Filesize = int64(torrent.TotalSize())
// extract filelist
fileInfos := torrent.Info.GetFiles()
for _, fileInfo := range fileInfos {
f.FileList = append(f.FileList, uploadedFile{
Path: fileInfo.Path,
Filesize: int64(fileInfo.Length),
})
}
} else {
// No torrent file provided
magnetURL, err := url.Parse(string(f.Magnet)) //?
if err != nil {
return err
}
xt := magnetURL.Query().Get("xt")
if !strings.HasPrefix(xt, "urn:btih:") {
return errors.New("Incorrect magnet")
}
xt = strings.SplitAfter(xt, ":")[2]
f.Infohash = strings.ToUpper(strings.Split(xt, "&")[0])
isBase32, err := regexp.MatchString("^[2-7A-Z]{32}$", f.Infohash)
if err != nil {
return err
}
if !isBase32 {
isBase16, err := regexp.MatchString("^[0-9A-F]{40}$", f.Infohash)
if err != nil {
return err
}
if !isBase16 {
return errors.New("Incorrect hash")
}
}
// TODO: Get Trackers from magnet URL
f.Filesize = 0
f.Filepath = ""
return nil
}
// then actually check that we have everything we need
if len(f.Name) == 0 {
return errInvalidTorrentName
}
// after data has been checked & extracted, write it to disk
if len(config.Conf.Torrents.FileStorage) > 0 {
err := writeTorrentToDisk(tfile, f.Infohash+".torrent", &f.Filepath)
if err != nil {
return err
}
} else {
f.Filepath = ""
}
return nil
}
func (f *uploadForm) ExtractEditInfo(r *http.Request) error {
f.Name = r.FormValue(uploadFormName)
f.Category = r.FormValue(uploadFormCategory)
f.WebsiteLink = r.FormValue(uploadFormWebsiteLink)
f.Description = r.FormValue(uploadFormDescription)
f.Hidden = r.FormValue(uploadFormHidden) == "on"
f.Status, _ = strconv.Atoi(r.FormValue(uploadFormStatus))
// trim whitespace
f.Name = strings.TrimSpace(f.Name)
f.Description = util.Sanitize(strings.TrimSpace(f.Description), "default")
defer r.Body.Close()
catsSplit := strings.Split(f.Category, "_")
// need this to prevent out of index panics
if len(catsSplit) == 2 {
CatID, err := strconv.Atoi(catsSplit[0])
if err != nil {
return errInvalidTorrentCategory
}
SubCatID, err := strconv.Atoi(catsSplit[1])
if err != nil {
return errInvalidTorrentCategory
}
if !categories.CategoryExists(f.Category) {
return errInvalidTorrentCategory
}
f.CategoryID = CatID
f.SubCategoryID = SubCatID
} else {
return errInvalidTorrentCategory
}
return nil
}
func writeTorrentToDisk(file multipart.File, name string, fullpath *string) error {
_, seekErr := file.Seek(0, io.SeekStart)
if seekErr != nil {
return seekErr
}
b, err := ioutil.ReadAll(file)
if err != nil {
return err
}
*fullpath = fmt.Sprintf("%s%c%s", config.Conf.Torrents.FileStorage, os.PathSeparator, name)
return ioutil.WriteFile(*fullpath, b, 0644)
}
// newUploadForm creates a new upload form given parameters as list
func newUploadForm(params ...string) (uploadForm uploadForm) {
if len(params) > 1 {
uploadForm.Category = params[0]
} else {
uploadForm.Category = "3_12"
}
if len(params) > 2 {
uploadForm.Description = params[1]
} else {
uploadForm.Description = "Description"
}
return
}

Voir le fichier

@ -1,30 +1,26 @@
package router
import (
"fmt"
"net/http"
"strconv"
"time"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/db"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service/api"
"github.com/NyaaPantsu/nyaa/service/captcha"
"github.com/NyaaPantsu/nyaa/service/notifier"
"github.com/NyaaPantsu/nyaa/service/torrent"
"github.com/NyaaPantsu/nyaa/service/upload"
"github.com/NyaaPantsu/nyaa/service/user"
"github.com/NyaaPantsu/nyaa/service/user/permission"
"github.com/NyaaPantsu/nyaa/util/log"
msg "github.com/NyaaPantsu/nyaa/util/messages"
"github.com/NyaaPantsu/nyaa/util/publicSettings"
)
// UploadHandler : Main Controller for uploading a torrent
func UploadHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
user := getUser(r)
if !uploadService.IsUploadEnabled(*user) {
if !uploadService.IsUploadEnabled(user) {
http.Error(w, "Error uploads are disabled", http.StatusBadRequest)
return
}
@ -38,7 +34,7 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
// UploadPostHandler : Controller for uploading a torrent, after POST request, redirect or makes error in messages
func UploadPostHandler(w http.ResponseWriter, r *http.Request) {
var uploadForm uploadForm
var uploadForm apiService.TorrentRequest
defer r.Body.Close()
user := getUser(r)
messages := msg.GetMessages(r) // new util for errors and infos
@ -62,14 +58,9 @@ func UploadPostHandler(w http.ResponseWriter, r *http.Request) {
status = model.TorrentStatusTrusted
}
torrentIndb := model.Torrent{}
db.ORM.Unscoped().Model(&model.Torrent{}).Where("torrent_hash = ?", uploadForm.Infohash).First(&torrentIndb)
if torrentIndb.ID > 0 {
if userPermission.CurrentUserIdentical(user, torrentIndb.UploaderID) && torrentIndb.IsDeleted() && !torrentIndb.IsBlocked() { // if torrent is not locked and is deleted and the user is the actual owner
torrentService.DefinitelyDeleteTorrent(strconv.Itoa(int(torrentIndb.ID)))
} else {
messages.AddError("errors", "Torrent already in database !")
}
err = torrentService.ExistOrDelete(uploadForm.Infohash, user)
if err != nil {
messages.AddError("errors", err.Error())
}
if !messages.HasErrors() {
@ -98,21 +89,7 @@ func UploadPostHandler(w http.ResponseWriter, r *http.Request) {
}
}
url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10))
if user.ID > 0 && config.Conf.Users.DefaultUserSettings["new_torrent"] { // If we are a member and notifications for new torrents are enabled
userService.GetFollowers(user) // We populate the liked field for users
if len(user.Followers) > 0 { // If we are followed by at least someone
for _, follower := range user.Followers {
follower.ParseSettings() // We need to call it before checking settings
if follower.Settings.Get("new_torrent") {
T, _, _ := publicSettings.TfuncAndLanguageWithFallback(follower.Language, follower.Language) // We need to send the notification to every user in their language
notifierService.NotifyUser(&follower, torrent.Identifier(), fmt.Sprintf(T("new_torrent_uploaded"), torrent.Name, user.Username), url.String(), follower.Settings.Get("new_torrent_email"))
}
}
}
}
torrentService.NewTorrentEvent(Router, user, &torrent)
// add filelist to files db, if we have one
if len(uploadForm.FileList) > 0 {
@ -126,6 +103,7 @@ func UploadPostHandler(w http.ResponseWriter, r *http.Request) {
}
}
url, err := Router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@ -139,7 +117,7 @@ func UploadGetHandler(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
messages := msg.GetMessages(r) // new util for errors and infos
var uploadForm uploadForm
var uploadForm apiService.TorrentRequest
_ = uploadForm.ExtractInfo(r)
user := getUser(r)
if userPermission.NeedsCaptcha(user) {

Voir le fichier

@ -14,6 +14,7 @@ import (
"github.com/NyaaPantsu/nyaa/db"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/api"
"github.com/NyaaPantsu/nyaa/service/captcha"
"github.com/NyaaPantsu/nyaa/service/notifier"
"github.com/NyaaPantsu/nyaa/service/report"
@ -172,7 +173,7 @@ func TorrentEditUserPanel(w http.ResponseWriter, r *http.Request) {
messages := msg.GetMessages(r)
currentUser := getUser(r)
if userPermission.CurrentOrAdmin(currentUser, torrent.UploaderID) {
uploadForm := newUploadForm()
uploadForm := apiService.NewTorrentRequest()
uploadForm.Name = torrent.Name
uploadForm.Category = strconv.Itoa(torrent.Category) + "_" + strconv.Itoa(torrent.SubCategory)
uploadForm.Remake = torrent.Status == model.TorrentStatusRemake
@ -190,7 +191,7 @@ func TorrentEditUserPanel(w http.ResponseWriter, r *http.Request) {
// TorrentPostEditUserPanel : Controller for editing a user torrent by a user, after post request
func TorrentPostEditUserPanel(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var uploadForm uploadForm
var uploadForm apiService.TorrentRequest
id := r.URL.Query().Get("id")
messages := msg.GetMessages(r)
torrent, _ := torrentService.GetTorrentByID(id)
@ -202,7 +203,6 @@ func TorrentPostEditUserPanel(w http.ResponseWriter, r *http.Request) {
}
if !messages.HasErrors() {
status := model.TorrentStatusNormal
uploadForm.Remake = r.FormValue(uploadFormRemake) == "on"
if uploadForm.Remake { // overrides trusted
status = model.TorrentStatusRemake
} else if currentUser.IsTrusted() {

Voir le fichier

@ -3,22 +3,63 @@ package apiService
import (
"encoding/base32"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/NyaaPantsu/nyaa/cache"
"github.com/NyaaPantsu/nyaa/config"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/upload"
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/categories"
"github.com/NyaaPantsu/nyaa/util/metainfo"
"github.com/zeebo/bencode"
)
// form names
const uploadFormName = "name"
const uploadFormTorrent = "torrent"
const uploadFormMagnet = "magnet"
const uploadFormCategory = "c"
const uploadFormRemake = "remake"
const uploadFormDescription = "desc"
const uploadFormWebsiteLink = "website_link"
const uploadFormStatus = "status"
const uploadFormHidden = "hidden"
// error indicating that you can't send both a magnet link and torrent
var errTorrentPlusMagnet = errors.New("Upload either a torrent file or magnet link, not both")
// error indicating a torrent is private
var errPrivateTorrent = errors.New("Torrent is private")
// error indicating a problem with its trackers
var errTrackerProblem = errors.New("Torrent does not have any (working) trackers: " + config.Conf.WebAddress.Nyaa + "/faq#trackers")
// error indicating a torrent's name is invalid
var errInvalidTorrentName = errors.New("Torrent name is invalid")
// error indicating a torrent's description is invalid
var errInvalidTorrentDescription = errors.New("Torrent description is invalid")
// error indicating a torrent's website link is invalid
var errInvalidWebsiteLink = errors.New("Website url or IRC link is invalid")
// error indicating a torrent's category is invalid
var errInvalidTorrentCategory = errors.New("Torrent category is invalid")
type torrentsQuery struct {
Category int `json:"category"`
SubCategory int `json:"sub_category"`
@ -34,17 +75,34 @@ type TorrentsRequest struct {
MaxPerPage int `json:"limit"`
}
// Use this, because we seem to avoid using models, and we would need
// the torrent ID to create the File in the DB
type uploadedFile struct {
Path []string `json:"path"`
Filesize int64 `json:"filesize"`
}
// TorrentRequest struct
//accept torrent files?
// Same json name as the constant!
type TorrentRequest struct {
Name string `json:"name"`
Category int `json:"category"`
SubCategory int `json:"sub_category"`
Magnet string `json:"magnet"`
Hash string `json:"hash"`
Description string `json:"description"`
Remake bool `json:"remake"`
WebsiteLink string `json:"website_link"`
Name string `json:"name,omitempty"`
Magnet string `json:"magnet,omitempty"`
Category string `json:"c"`
Remake bool `json:"remake,omitempty"`
Description string `json:"desc,omitempty"`
Status int `json:"status,omitempty"`
Hidden bool `json:"hidden,omitempty"`
CaptchaID string `json:"-"`
WebsiteLink string `json:"website_link,omitempty"`
SubCategory int `json:"sub_category,omitempty"`
Infohash string `json:"hash,omitempty"`
CategoryID int `json:"-"`
SubCategoryID int `json:"-"`
Filesize int64 `json:"filesize,omitempty"`
Filepath string `json:"-"`
FileList []uploadedFile `json:"filelist,omitempty"`
Trackers []string `json:"trackers,omitempty"`
}
// UpdateRequest struct
@ -73,16 +131,24 @@ func (r *TorrentsRequest) ToParams() serviceBase.WhereParams {
return res
}
func validateName(r *TorrentRequest) (error, int) {
/*if len(r.Name) < 100 { //isn't this too much?
return ErrShortName, http.StatusNotAcceptable
}*/
return nil, http.StatusOK
func (r *TorrentRequest) validateName() error {
// then actually check that we have everything we need
if len(r.Name) == 0 {
return errInvalidTorrentName
}
return nil
}
func (r *TorrentRequest) validateDescription() error {
if len(r.Description) > 500 {
return errInvalidTorrentDescription
}
return nil
}
// TODO Check category is within accepted range
func validateCategory(r *TorrentRequest) (error, int) {
if r.Category == 0 {
if r.CategoryID == 0 {
return ErrCategory, http.StatusNotAcceptable
}
return nil, http.StatusOK
@ -107,157 +173,265 @@ func validateWebsiteLink(r *TorrentRequest) (error, int) {
return nil, http.StatusOK
}
func validateMagnet(r *TorrentRequest) (error, int) {
func (r *TorrentRequest) validateMagnet() error {
magnetURL, err := url.Parse(string(r.Magnet)) //?
if err != nil {
return err, http.StatusInternalServerError
return err
}
xt := magnetURL.Query().Get("xt")
if !strings.HasPrefix(xt, "urn:btih:") {
return ErrMagnet, http.StatusNotAcceptable
return ErrMagnet
}
xt = strings.SplitAfter(xt, ":")[2]
r.Hash = strings.ToUpper(strings.Split(xt, "&")[0])
fmt.Println(r.Hash)
return nil, http.StatusOK
r.Infohash = strings.ToUpper(strings.Split(xt, "&")[0])
return nil
}
func validateHash(r *TorrentRequest) (error, int) {
r.Hash = strings.ToUpper(r.Hash)
isBase32, err := regexp.MatchString("^[2-7A-Z]{32}$", r.Hash)
func (r *TorrentRequest) validateHash() error {
isBase32, err := regexp.MatchString("^[2-7A-Z]{32}$", r.Infohash)
if err != nil {
return err, http.StatusInternalServerError
return err
}
if !isBase32 {
isBase16, err := regexp.MatchString("^[0-9A-F]{40}$", r.Hash)
isBase16, err := regexp.MatchString("^[0-9A-F]{40}$", r.Infohash)
if err != nil {
return err, http.StatusInternalServerError
return err
}
if !isBase16 {
return ErrHash, http.StatusNotAcceptable
return ErrHash
}
} else {
//convert to base16
data, err := base32.StdEncoding.DecodeString(r.Hash)
data, err := base32.StdEncoding.DecodeString(r.Infohash)
if err != nil {
return err, http.StatusInternalServerError
return err
}
hash16 := make([]byte, hex.EncodedLen(len(data)))
hex.Encode(hash16, data)
r.Hash = strings.ToUpper(string(hash16))
r.Infohash = strings.ToUpper(string(hash16))
}
return nil, http.StatusOK
return nil
}
// ValidateUpload : Check if an upload is valid
func (r *TorrentRequest) ValidateUpload() (err error, code int) {
validators := []func(r *TorrentRequest) (error, int){
validateName,
validateCategory,
validateSubCategory,
validateMagnet,
validateHash,
validateWebsiteLink,
// ExtractEditInfo : takes an http request and computes all fields for this form
func (r *TorrentRequest) ExtractEditInfo(req *http.Request) error {
err := r.ExtractBasicValue(req)
if err != nil {
return err
}
for i, validator := range validators {
if r.Hash != "" && i == 3 {
continue
}
err, code = validator(r)
defer req.Body.Close()
err = r.ExtractCategory(req)
if err != nil {
return err
}
return nil
}
// ExtractCategory : takes an http request and computes category field for this form
func (r *TorrentRequest) ExtractCategory(req *http.Request) error {
catsSplit := strings.Split(r.Category, "_")
// need this to prevent out of index panics
if len(catsSplit) == 2 {
CatID, err := strconv.Atoi(catsSplit[0])
if err != nil {
break
return errInvalidTorrentCategory
}
SubCatID, err := strconv.Atoi(catsSplit[1])
if err != nil {
return errInvalidTorrentCategory
}
if !categories.CategoryExists(r.Category) {
return errInvalidTorrentCategory
}
r.CategoryID = CatID
r.SubCategoryID = SubCatID
} else {
return errInvalidTorrentCategory
}
return nil
}
// ExtractBasicValue : takes an http request and computes all basic fields for this form
func (r *TorrentRequest) ExtractBasicValue(req *http.Request) error {
if strings.HasPrefix(req.Header.Get("Content-type"), "multipart/form-data") || req.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { // Multipart
if strings.HasPrefix(req.Header.Get("Content-type"), "multipart/form-data") { // We parse the multipart form
err := req.ParseMultipartForm(15485760)
if err != nil {
return err
}
}
r.Name = req.FormValue(uploadFormName)
r.Category = req.FormValue(uploadFormCategory)
r.WebsiteLink = req.FormValue(uploadFormWebsiteLink)
r.Description = req.FormValue(uploadFormDescription)
r.Hidden = req.FormValue(uploadFormHidden) == "on"
r.Status, _ = strconv.Atoi(req.FormValue(uploadFormStatus))
r.Remake = req.FormValue(uploadFormRemake) == "on"
r.Magnet = req.FormValue(uploadFormMagnet)
} else { // JSON (no file upload then)
decoder := json.NewDecoder(req.Body)
err := decoder.Decode(&r)
if err != nil {
return err
}
}
return err, code
// trim whitespace
r.Name = strings.TrimSpace(r.Name)
r.Description = util.Sanitize(strings.TrimSpace(r.Description), "default")
r.WebsiteLink = strings.TrimSpace(r.WebsiteLink)
r.Magnet = strings.TrimSpace(r.Magnet)
// then actually check that we have everything we need
err := r.validateName()
if err != nil {
return err
}
err = r.validateDescription()
if err != nil {
return err
}
if r.WebsiteLink != "" {
// WebsiteLink
urlRegexp, _ := regexp.Compile(`^(https?:\/\/|ircs?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$`)
if !urlRegexp.MatchString(r.WebsiteLink) {
return errInvalidWebsiteLink
}
}
return nil
}
// ExtractInfo : takes an http request and computes all fields for this form
func (r *TorrentRequest) ExtractInfo(req *http.Request) error {
err := r.ExtractBasicValue(req)
if err != nil {
return err
}
cache.Impl.ClearAll()
defer req.Body.Close()
err = r.ExtractCategory(req)
if err != nil {
return err
}
tfile, err := r.ValidateMultipartUpload(req)
if err != nil {
return err
}
// after data has been checked & extracted, write it to disk
if len(config.Conf.Torrents.FileStorage) > 0 {
err := writeTorrentToDisk(tfile, r.Infohash+".torrent", &r.Filepath)
if err != nil {
return err
}
} else {
r.Filepath = ""
}
return nil
}
// ValidateMultipartUpload : Check if multipart upload is valid
func (r *TorrentRequest) ValidateMultipartUpload(req *http.Request) (int64, error, int) {
tfile, _, err := req.FormFile("torrent")
func (r *TorrentRequest) ValidateMultipartUpload(req *http.Request) (multipart.File, error) {
// first: parse torrent file (if any) to fill missing information
tfile, _, err := req.FormFile(uploadFormTorrent)
if err == nil {
var torrent metainfo.TorrentFile
// decode torrent
if _, err = tfile.Seek(0, io.SeekStart); err != nil {
return 0, err, http.StatusInternalServerError
_, seekErr := tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return tfile, seekErr
}
if err = bencode.NewDecoder(tfile).Decode(&torrent); err != nil {
return 0, err, http.StatusInternalServerError
err = bencode.NewDecoder(tfile).Decode(&torrent)
if err != nil {
return tfile, metainfo.ErrInvalidTorrentFile
}
// check a few things
if torrent.IsPrivate() {
return 0, errors.New("private torrents not allowed"), http.StatusNotAcceptable
return tfile, errPrivateTorrent
}
trackers := torrent.GetAllAnnounceURLS()
trackers = uploadService.CheckTrackers(trackers)
if len(trackers) == 0 {
return 0, errors.New("tracker(s) not allowed"), http.StatusNotAcceptable
r.Trackers = uploadService.CheckTrackers(trackers)
if len(r.Trackers) == 0 {
return tfile, errTrackerProblem
}
if r.Name == "" {
// Name
if len(r.Name) == 0 {
r.Name = torrent.TorrentName()
}
_, err = tfile.Seek(0, io.SeekStart)
if err != nil {
return 0, err, http.StatusInternalServerError
// Magnet link: if a file is provided it should be empty
if len(r.Magnet) != 0 {
return tfile, errTorrentPlusMagnet
}
_, seekErr = tfile.Seek(0, io.SeekStart)
if seekErr != nil {
return tfile, seekErr
}
infohash, err := metainfo.DecodeInfohash(tfile)
if err != nil {
return 0, err, http.StatusInternalServerError
return tfile, metainfo.ErrInvalidTorrentFile
}
r.Hash = infohash
r.Infohash = infohash
r.Magnet = util.InfoHashToMagnet(infohash, r.Name, trackers...)
// extract filesize
filesize := int64(torrent.TotalSize())
err, code := r.ValidateUpload()
return filesize, err, code
}
return 0, err, http.StatusInternalServerError
}
r.Filesize = int64(torrent.TotalSize())
// ValidateUpdate : Check if an update is valid
func (r *TorrentRequest) ValidateUpdate() (err error, code int) {
validators := []func(r *TorrentRequest) (error, int){
validateName,
validateCategory,
validateSubCategory,
validateMagnet,
validateHash,
validateWebsiteLink,
}
//don't update not requested values
//rewrite with reflect?
for i, validator := range validators {
if (r.Name == "" && i == 0) || (r.Category == 0 && i == 1) ||
(r.SubCategory == 0 && i == 2) ||
(r.Hash != "" || r.Magnet == "" && i == 3) || (r.Hash == "" && i == 4) {
continue
// extract filelist
fileInfos := torrent.Info.GetFiles()
for _, fileInfo := range fileInfos {
r.FileList = append(r.FileList, uploadedFile{
Path: fileInfo.Path,
Filesize: int64(fileInfo.Length),
})
}
err, code = validator(r)
} else {
err = r.validateMagnet()
if err != nil {
break
return tfile, err
}
}
err = r.validateHash()
if err != nil {
return tfile, err
}
// TODO: Get Trackers from magnet URL
r.Filesize = 0
r.Filepath = ""
return err, code
return tfile, nil
}
return tfile, err
}
// UpdateTorrent : Update torrent model
//rewrite with reflect ?
func (r *UpdateRequest) UpdateTorrent(t *model.Torrent) {
func (r *UpdateRequest) UpdateTorrent(t *model.Torrent, currentUser *model.User) {
if r.Update.Name != "" {
t.Name = r.Update.Name
}
if r.Update.Hash != "" {
t.Hash = r.Update.Hash
if r.Update.Infohash != "" {
t.Hash = r.Update.Infohash
}
if r.Update.Category != 0 {
t.Category = r.Update.Category
if r.Update.CategoryID != 0 {
t.Category = r.Update.CategoryID
}
if r.Update.SubCategory != 0 {
t.SubCategory = r.Update.SubCategory
if r.Update.SubCategoryID != 0 {
t.SubCategory = r.Update.SubCategoryID
}
if r.Update.Description != "" {
t.Description = r.Update.Description
@ -265,4 +439,39 @@ func (r *UpdateRequest) UpdateTorrent(t *model.Torrent) {
if r.Update.WebsiteLink != "" {
t.WebsiteLink = r.Update.WebsiteLink
}
status := model.TorrentStatusNormal
if r.Update.Remake { // overrides trusted
status = model.TorrentStatusRemake
} else if currentUser.IsTrusted() {
status = model.TorrentStatusTrusted
}
t.Status = status
}
func writeTorrentToDisk(file multipart.File, name string, fullpath *string) error {
_, seekErr := file.Seek(0, io.SeekStart)
if seekErr != nil {
return seekErr
}
b, err := ioutil.ReadAll(file)
if err != nil {
return err
}
*fullpath = fmt.Sprintf("%s%c%s", config.Conf.Torrents.FileStorage, os.PathSeparator, name)
return ioutil.WriteFile(*fullpath, b, 0644)
}
// NewTorrentRequest : creates a new torrent request struc with some default value
func NewTorrentRequest(params ...string) (torrentRequest TorrentRequest) {
if len(params) > 1 {
torrentRequest.Category = params[0]
} else {
torrentRequest.Category = "3_12"
}
if len(params) > 2 {
torrentRequest.Description = params[1]
} else {
torrentRequest.Description = "Description"
}
return
}

Voir le fichier

@ -2,6 +2,7 @@ package torrentService
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
@ -10,8 +11,13 @@ import (
"github.com/NyaaPantsu/nyaa/db"
"github.com/NyaaPantsu/nyaa/model"
"github.com/NyaaPantsu/nyaa/service"
"github.com/NyaaPantsu/nyaa/service/notifier"
"github.com/NyaaPantsu/nyaa/service/user"
"github.com/NyaaPantsu/nyaa/service/user/permission"
"github.com/NyaaPantsu/nyaa/util"
"github.com/NyaaPantsu/nyaa/util/log"
"github.com/NyaaPantsu/nyaa/util/publicSettings"
"github.com/gorilla/mux"
)
/* Function to interact with Models
@ -296,3 +302,41 @@ func GetDeletedTorrents(parameters *serviceBase.WhereParams, orderBy string, lim
torrents, count, err = getTorrentsOrderBy(parameters, orderBy, limit, offset, true, true, true)
return
}
// ExistOrDelete : Check if a torrent exist with the same hash and if it can be replaced, it is replaced
func ExistOrDelete(hash string, user *model.User) error {
torrentIndb := model.Torrent{}
db.ORM.Unscoped().Model(&model.Torrent{}).Where("torrent_hash = ?", hash).First(&torrentIndb)
if torrentIndb.ID > 0 {
if userPermission.CurrentUserIdentical(user, torrentIndb.UploaderID) && torrentIndb.IsDeleted() && !torrentIndb.IsBlocked() { // if torrent is not locked and is deleted and the user is the actual owner
_, err := DefinitelyDeleteTorrent(strconv.Itoa(int(torrentIndb.ID)))
if err != nil {
return err
}
} else {
return errors.New("Torrent already in database")
}
}
return nil
}
// NewTorrentEvent : Should be called when you create a new torrent
func NewTorrentEvent(router *mux.Router, user *model.User, torrent *model.Torrent) error {
url, err := router.Get("view_torrent").URL("id", strconv.FormatUint(uint64(torrent.ID), 10))
if err != nil {
return err
}
if user.ID > 0 && config.Conf.Users.DefaultUserSettings["new_torrent"] { // If we are a member and notifications for new torrents are enabled
userService.GetFollowers(user) // We populate the liked field for users
if len(user.Followers) > 0 { // If we are followed by at least someone
for _, follower := range user.Followers {
follower.ParseSettings() // We need to call it before checking settings
if follower.Settings.Get("new_torrent") {
T, _, _ := publicSettings.TfuncAndLanguageWithFallback(follower.Language, follower.Language) // We need to send the notification to every user in their language
notifierService.NotifyUser(&follower, torrent.Identifier(), fmt.Sprintf(T("new_torrent_uploaded"), torrent.Name, user.Username), url.String(), follower.Settings.Get("new_torrent_email"))
}
}
}
}
return nil
}

Voir le fichier

@ -48,7 +48,7 @@ func CheckTrackers(trackers []string) []string {
}
// IsUploadEnabled : Check if upload is enabled in config
func IsUploadEnabled(u model.User) bool {
func IsUploadEnabled(u *model.User) bool {
if config.Conf.Torrents.UploadsDisabled {
if config.Conf.Torrents.AdminsAreStillAllowedTo && u.IsModerator() {
return true

Voir le fichier

@ -269,32 +269,37 @@ func RetrieveCurrentUser(r *http.Request) (model.User, int, error) {
}
// RetrieveUserByEmail retrieves a user by an email
func RetrieveUserByEmail(email string) (*model.PublicUser, string, int, error) {
func RetrieveUserByEmail(email string) (*model.User, string, int, error) {
var user model.User
if db.ORM.Unscoped().Where("email = ?", email).First(&user).RecordNotFound() {
return &model.PublicUser{User: &user}, email, http.StatusNotFound, errors.New("user not found")
return &user, email, http.StatusNotFound, errors.New("user not found")
}
return &model.PublicUser{User: &user}, email, http.StatusOK, nil
return &user, email, http.StatusOK, nil
}
// RetrieveUserByAPIToken retrieves a user by an API token
func RetrieveUserByAPIToken(apiToken string) (*model.User, string, int, error) {
var user model.User
if db.ORM.Unscoped().Where("api_token = ?", apiToken).First(&user).RecordNotFound() {
return &user, apiToken, http.StatusNotFound, errors.New("user not found")
}
return &user, apiToken, http.StatusOK, nil
}
// RetrieveUsersByEmail retrieves users by an email
func RetrieveUsersByEmail(email string) []*model.PublicUser {
func RetrieveUsersByEmail(email string) []*model.User {
var users []*model.User
var userArr []*model.PublicUser
db.ORM.Where("email = ?", email).Find(&users)
for _, user := range users {
userArr = append(userArr, &model.PublicUser{User: user})
}
return userArr
return users
}
// RetrieveUserByUsername retrieves a user by username.
func RetrieveUserByUsername(username string) (*model.PublicUser, string, int, error) {
func RetrieveUserByUsername(username string) (*model.User, string, int, error) {
var user model.User
if db.ORM.Where("username = ?", username).First(&user).RecordNotFound() {
return &model.PublicUser{User: &user}, username, http.StatusNotFound, errors.New("user not found")
return &user, username, http.StatusNotFound, errors.New("user not found")
}
return &model.PublicUser{User: &user}, username, http.StatusOK, nil
return &user, username, http.StatusOK, nil
}
// RetrieveOldUploadsByUsername retrieves olduploads by username