Skip to content

Commit

Permalink
feat(drivers): add the support for Trainbit (#3813)
Browse files Browse the repository at this point in the history
* feat: add the support for Trainbit
read only

* feat: add the support for Trainbit
modify the structure of code
allow to create folder, move, rename and remove

* feat: add the support for Trainbit
allow to upload file

* feat: add the support for Trainbit
get token from page

* feat: add the support for Trainbit
display progress of updating

* feat: add the support for Trainbit
fix bug of time zone

* feat: add the support for Trainbit
fix the bug of filename
  • Loading branch information
NightSpaceC authored Mar 12, 2023
1 parent 2a601f0 commit 3b2703a
Show file tree
Hide file tree
Showing 5 changed files with 323 additions and 0 deletions.
1 change: 1 addition & 0 deletions drivers/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/terabox"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/trainbit"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/webdav"
Expand Down
142 changes: 142 additions & 0 deletions drivers/trainbit/driver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package trainbit

import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
"strings"

"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
)

type Trainbit struct {
model.Storage
Addition
}

var apiExpiredate, guid string

func (d *Trainbit) Config() driver.Config {
return config
}

func (d *Trainbit) GetAddition() driver.Additional {
return &d.Addition
}

func (d *Trainbit) Init(ctx context.Context) error {
http.DefaultClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
var err error
apiExpiredate, guid, err = getToken(d.ApiKey, d.AUSHELLPORTAL)
if err != nil {
return err
}
return nil
}

func (d *Trainbit) Drop(ctx context.Context) error {
return nil
}

func (d *Trainbit) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
form := make(url.Values)
form.Set("parentid", strings.Split(dir.GetID(), "_")[0])
res, err := postForm("https://trainbit.com/lib/api/v1/listoffiles", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
if err != nil {
return nil, err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var jsonData any
json.Unmarshal(data, &jsonData)
if err != nil {
return nil, err
}
object, err := parseRawFileObject(jsonData.(map[string]any)["items"].([]any))
if err != nil {
return nil, err
}
return object, nil
}

func (d *Trainbit) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
res, err := get(fmt.Sprintf("https://trainbit.com/files/%s/", strings.Split(file.GetID(), "_")[0]), d.ApiKey, d.AUSHELLPORTAL)
if err != nil {
return nil, err
}
return &model.Link{
URL: res.Header.Get("Location"),
}, nil
}

func (d *Trainbit) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
form := make(url.Values)
form.Set("name", local2provider(dirName, true))
form.Set("parentid", strings.Split(parentDir.GetID(), "_")[0])
_, err := postForm("https://trainbit.com/lib/api/v1/createfolder", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
return err
}

func (d *Trainbit) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
form := make(url.Values)
form.Set("sourceid", strings.Split(srcObj.GetID(), "_")[0])
form.Set("destinationid", strings.Split(dstDir.GetID(), "_")[0])
_, err := postForm("https://trainbit.com/lib/api/v1/move", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
return err
}

func (d *Trainbit) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
form := make(url.Values)
form.Set("id", strings.Split(srcObj.GetID(), "_")[0])
form.Set("name", local2provider(newName, srcObj.IsDir()))
_, err := postForm("https://trainbit.com/lib/api/v1/edit", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
return err
}

func (d *Trainbit) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}

func (d *Trainbit) Remove(ctx context.Context, obj model.Obj) error {
form := make(url.Values)
form.Set("id", strings.Split(obj.GetID(), "_")[0])
_, err := postForm("https://trainbit.com/lib/api/v1/delete", form, apiExpiredate, d.ApiKey, d.AUSHELLPORTAL)
return err
}

func (d *Trainbit) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
endpoint, _ := url.Parse("https://tb28.trainbit.com/api/upload/send_raw/")
query := &url.Values{}
query.Add("q", strings.Split(dstDir.GetID(), "_")[1])
query.Add("guid", guid)
query.Add("name", url.QueryEscape(local2provider(stream.GetName(), false)))
endpoint.RawQuery = query.Encode()
var total int64
total = 0
progressReader := &ProgressReader{
stream,
func(byteNum int) {
total += int64(byteNum)
up(int(math.Round(float64(total) / float64(stream.GetSize()) * 100)))
},
}
req, err := http.NewRequest(http.MethodPost, endpoint.String(), progressReader)
if err != nil {
return err
}
req.Header.Set("Content-Type", "text/json; charset=UTF-8")
_, err = http.DefaultClient.Do(req)
return err
}

var _ driver.Driver = (*Trainbit)(nil)
29 changes: 29 additions & 0 deletions drivers/trainbit/meta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package trainbit

import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)

type Addition struct {
driver.RootID
AUSHELLPORTAL string `json:"AUSHELLPORTAL" required:"true"`
ApiKey string `json:"apikey" required:"true"`
}

var config = driver.Config{
Name: "Trainbit",
LocalSort: false,
OnlyLocal: false,
OnlyProxy: false,
NoCache: false,
NoUpload: false,
NeedMs: false,
DefaultRoot: "0_000",
}

func init() {
op.RegisterDriver(func() driver.Driver {
return &Trainbit{}
})
}
1 change: 1 addition & 0 deletions drivers/trainbit/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package trainbit
150 changes: 150 additions & 0 deletions drivers/trainbit/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package trainbit

import (
"io"
"net/http"
"net/url"
"regexp"
"strings"
"time"

"github.com/alist-org/alist/v3/internal/model"
)

type ProgressReader struct {
io.Reader
reporter func(byteNum int)
}

func (progressReader *ProgressReader) Read(data []byte) (int, error) {
byteNum, err := progressReader.Reader.Read(data)
progressReader.reporter(byteNum)
return byteNum, err
}

func get(url string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.AddCookie(&http.Cookie{
Name: ".AUSHELLPORTAL",
Value: AUSHELLPORTAL,
MaxAge: 2 * 60,
})
req.AddCookie(&http.Cookie{
Name: "retkeyapi",
Value: apiKey,
MaxAge: 2 * 60,
})
res, err := http.DefaultClient.Do(req)
return res, err
}

func postForm(endpoint string, data url.Values, apiExpiredate string, apiKey string, AUSHELLPORTAL string) (*http.Response, error) {
extData := make(url.Values)
for key, value := range data {
extData[key] = make([]string, len(value))
copy(extData[key], value)
}
extData.Set("apikey", apiKey)
extData.Set("expiredate", apiExpiredate)
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(extData.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.AddCookie(&http.Cookie{
Name: ".AUSHELLPORTAL",
Value: AUSHELLPORTAL,
MaxAge: 2 * 60,
})
req.AddCookie(&http.Cookie{
Name: "retkeyapi",
Value: apiKey,
MaxAge: 2 * 60,
})
res, err := http.DefaultClient.Do(req)
return res, err
}

func getToken(apiKey string, AUSHELLPORTAL string) (string, string, error) {
res, err := get("https://trainbit.com/files/", apiKey, AUSHELLPORTAL)
if err != nil {
return "", "", err
}
data, err := io.ReadAll(res.Body)
if err != nil {
return "", "", err
}
text := string(data)
apiExpiredateReg := regexp.MustCompile(`core.api.expiredate = '([^']*)';`)
result := apiExpiredateReg.FindAllStringSubmatch(text, -1)
apiExpiredate := result[0][1]
guidReg := regexp.MustCompile(`app.vars.upload.guid = '([^']*)';`)
result = guidReg.FindAllStringSubmatch(text, -1)
guid := result[0][1]
return apiExpiredate, guid, nil
}

func local2provider(filename string, isFolder bool) string {
filename = strings.Replace(filename, "%", url.QueryEscape("%"), -1)
filename = strings.Replace(filename, "/", url.QueryEscape("/"), -1)
filename = strings.Replace(filename, ":", url.QueryEscape(":"), -1)
filename = strings.Replace(filename, "*", url.QueryEscape("*"), -1)
filename = strings.Replace(filename, "?", url.QueryEscape("?"), -1)
filename = strings.Replace(filename, "\"", url.QueryEscape("\""), -1)
filename = strings.Replace(filename, "<", url.QueryEscape("<"), -1)
filename = strings.Replace(filename, ">", url.QueryEscape(">"), -1)
filename = strings.Replace(filename, "|", url.QueryEscape("|"), -1)
if isFolder {
return filename
}
return strings.Join([]string{filename, ".delete_suffix."}, "")
}

func provider2local(filename string) string {
index := strings.LastIndex(filename, ".delete_suffix.")
if index != -1 {
filename = filename[:index]
}
rawName := strings.Replace(filename, url.QueryEscape("/"), "/", -1)
rawName = strings.Replace(rawName, url.QueryEscape(":"), ":", -1)
rawName = strings.Replace(rawName, url.QueryEscape("*"), "*", -1)
rawName = strings.Replace(rawName, url.QueryEscape("?"), "?", -1)
rawName = strings.Replace(rawName, url.QueryEscape("\""), "\"", -1)
rawName = strings.Replace(rawName, url.QueryEscape("<"), "<", -1)
rawName = strings.Replace(rawName, url.QueryEscape(">"), ">", -1)
rawName = strings.Replace(rawName, url.QueryEscape("|"), "|", -1)
rawName = strings.Replace(rawName, url.QueryEscape("%"), "%", -1)
return rawName
}

func parseRawFileObject(rawObject []any) ([]model.Obj, error) {
objectList := make([]model.Obj, 0)
for _, each := range rawObject {
object := each.(map[string]any)
if object["id"].(string) == "0" {
continue
}
isFolder := int64(object["ty"].(float64)) == 1
var name string
if isFolder {
name = object["name"].(string)
} else {
name = strings.Join([]string{object["name"].(string), object["ext"].(string)}, ".")
}
modified, err := time.Parse("2006/01/02 15:04:05", object["modified"].(string))
if err != nil {
return nil, err
}
objectList = append(objectList, model.Obj(&model.Object{
ID: strings.Join([]string{object["id"].(string), strings.Split(object["uploadurl"].(string), "=")[1]}, "_"),
Name: provider2local(name),
Size: int64(object["byte"].(float64)),
Modified: modified.Add(-210 * time.Minute),
IsFolder: isFolder,
}))
}
return objectList, nil
}

0 comments on commit 3b2703a

Please sign in to comment.