Compare commits

...

18 Commits

Author SHA1 Message Date
efe0e6af22 feat: silent start, stop and restart 2022-11-11 18:42:06 +08:00
00de9bf16d fix!: sign with the raw path instead of filename (#2258) 2022-11-11 16:24:25 +08:00
1743110a70 fix(123): incorrect order_by (close #2285) 2022-11-10 21:47:13 +08:00
0352a8e028 feat: add alist v2 driver (#2281) 2022-11-10 17:42:12 +08:00
c601bb794b feat(123): support mail login (close #2218 pr #2276) 2022-11-10 09:34:48 +08:00
42865486f1 fix(local): deal with relative symlink dir (#2274) 2022-11-09 18:15:42 +08:00
44f5cf40ef fix(115): update 115 driver lib to fix some bugs (#2275)
* fix duplicate cookies in client.List func
* rm useless cookie when init
2022-11-09 18:15:06 +08:00
c3ab378ac5 feat(google_drive): support shortcut (close #2268) 2022-11-09 16:19:33 +08:00
cdcbfb24c4 fix(local): directory handle (#2262)
* fix(local): check symlink dir

* fix(local): set size of dir to 0 (close #2264)
2022-11-09 11:20:09 +08:00
e05e2fd663 chore: change default timeout (close #2252) 2022-11-08 20:37:42 +08:00
6639cab1ae feat(google_drive): chunk upload (close #2241) 2022-11-07 20:58:52 +08:00
8241f0999a feat(google_drive): override upload (close #1880) 2022-11-07 20:35:35 +08:00
f3a5e3702d fix(189): force use CN time zone (close #2240) 2022-11-07 16:37:47 +08:00
46701a176d feat(aria2): mark aria2 seeding as complete (#2223)
Currently if using aria2 to download a torrent file, it does not
consider seeding + active as completed, so the torrent download task
only completes as aria2 stops seeding.

This commit uses seeder property of TaskInfo, and mark tasks with active
status and true seeder as complete.
2022-11-06 16:20:09 +08:00
26a29f20c3 fix: missed encode path while use down proxy (close #2208) 2022-11-06 14:46:47 +08:00
18cd45d257 fix: disable cache for 302 redirect (close #2216) 2022-11-05 15:54:51 +08:00
f0a533a77a feat(115): put UA as a variable (#2217)
In special cases, developers can pass in custom UA to solve the speed limit problem
Mainly for developers calling from outside
2022-11-05 15:50:57 +08:00
619a9aeb6c feat(115): add qrcode login (#2206) 2022-11-04 21:16:52 +08:00
33 changed files with 599 additions and 61 deletions

1
.gitignore vendored
View File

@ -25,5 +25,6 @@ bin/*
data/ data/
log/ log/
lang/ lang/
daemon/
public/dist/* public/dist/*
!public/dist/README.md !public/dist/README.md

View File

@ -1,8 +1,14 @@
package cmd package cmd
import ( import (
"os"
"path/filepath"
"strconv"
"github.com/alist-org/alist/v3/internal/bootstrap" "github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data" "github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
) )
func Init() { func Init() {
@ -11,3 +17,27 @@ func Init() {
bootstrap.InitDB() bootstrap.InitDB()
data.InitData() data.InitData()
} }
var pid = -1
var pidFile string
func initDaemon() {
ex, err := os.Executable()
if err != nil {
log.Fatal(err)
}
exPath := filepath.Dir(ex)
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
pidFile = filepath.Join(exPath, "daemon/pid")
if utils.Exists(pidFile) {
bytes, err := os.ReadFile(pidFile)
if err != nil {
log.Fatal("failed to read pid file", err)
}
id, err := strconv.Atoi(string(bytes))
if err != nil {
log.Fatal("failed to parse pid data", err)
}
pid = id
}
}

32
cmd/restart.go Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"github.com/spf13/cobra"
)
// restartCmd represents the restart command
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart alist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
stop()
start()
},
}
func init() {
rootCmd.AddCommand(restartCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// restartCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

71
cmd/start.go Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"os/exec"
"path/filepath"
"strconv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "Silent start alist server with `--force-bin-dir`",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
func start() {
initDaemon()
if pid != -1 {
_, err := os.FindProcess(pid)
if err == nil {
log.Info("alist already started, pid ", pid)
return
}
}
args := os.Args
args[1] = "server"
args = append(args, "--force-bin-dir")
cmd := &exec.Cmd{
Path: args[0],
Args: args,
Env: os.Environ(),
}
stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Fatal(os.Getpid(), ": failed to open start log file:", err)
}
cmd.Stderr = stdout
cmd.Stdout = stdout
err = cmd.Start()
if err != nil {
log.Fatal("failed to start children process: ", err)
}
log.Infof("success start pid: %d", cmd.Process.Pid)
err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
if err != nil {
log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`")
}
}
func init() {
rootCmd.AddCommand(startCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// startCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

58
cmd/stop.go Normal file
View File

@ -0,0 +1,58 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// stopCmd represents the stop command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop alist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
stop()
},
}
func stop() {
initDaemon()
if pid == -1 {
log.Info("Seems not have been started. Try use `alist start` to start server.")
return
}
process, err := os.FindProcess(pid)
if err != nil {
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
return
}
err = process.Kill()
if err != nil {
log.Errorf("failed to kill process %d: %v", pid, err)
} else {
log.Info("killed process: ", pid)
}
err = os.Remove(pidFile)
if err != nil {
log.Errorf("failed to remove pid file")
}
pid = -1
}
func init() {
rootCmd.AddCommand(stopCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -6,7 +6,8 @@ import (
) )
type Addition struct { type Addition struct {
Cookie string `json:"cookie" required:"true"` Cookie string `json:"cookie"`
QRCodeToken string `json:"qrcode_token"`
driver.RootID driver.RootID
} }

View File

@ -1,23 +1,38 @@
package _115 package _115
import ( import (
"fmt"
"github.com/SheltonZhu/115driver/pkg/driver" "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/drivers/base" "github.com/pkg/errors"
) )
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 115Browser/23.9.3.2 115disk/30.1.0"
func (d *Pan115) login() error { func (d *Pan115) login() error {
var err error
opts := []driver.Option{ opts := []driver.Option{
driver.WithRestyClient(base.RestyClient), driver.UA(UserAgent),
driver.UA("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 115Browser/23.9.3.2 115disk/30.1.0"),
} }
d.client = driver.New(opts...) d.client = driver.New(opts...)
cr := &driver.Credential{} cr := &driver.Credential{}
if err := cr.FromCookie(d.Addition.Cookie); err != nil { if d.Addition.QRCodeToken != "" {
return err s := &driver.QRCodeSession{
UID: d.Addition.QRCodeToken,
}
if cr, err = d.client.QRCodeLogin(s); err != nil {
return errors.Wrap(err, "failed to login by qrcode")
}
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.Addition.QRCodeToken = ""
} else if d.Addition.Cookie != "" {
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
} }
d.client.ImportCredential(cr)
return d.client.LoginCheck() return d.client.LoginCheck()
} }

View File

@ -8,7 +8,7 @@ import (
type Addition struct { type Addition struct {
Username string `json:"username" required:"true"` Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"` Password string `json:"password" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,fileId,updateAt,createAt" default:"name"` OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
driver.RootID driver.RootID
// define other // define other

View File

@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
@ -13,14 +14,24 @@ import (
// do others that not defined in Driver interface // do others that not defined in Driver interface
func (d *Pan123) login() error { func (d *Pan123) login() error {
var body base.Json
url := "https://www.123pan.com/api/user/sign_in" url := "https://www.123pan.com/api/user/sign_in"
if utils.IsEmailFormat(d.Username) {
body = base.Json{
"mail": d.Username,
"password": d.Password,
"type": 2,
}
} else {
body = base.Json{
"passport": d.Username,
"password": d.Password,
}
}
var resp TokenResp var resp TokenResp
_, err := base.RestyClient.R(). _, err := base.RestyClient.R().
SetResult(&resp). SetResult(&resp).
SetBody(base.Json{ SetBody(body).Post(url)
"passport": d.Username,
"password": d.Password,
}).Post(url)
if err != nil { if err != nil {
return err return err
} }

View File

@ -178,7 +178,6 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) { func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
res := make([]model.Obj, 0) res := make([]model.Obj, 0)
pageNum := 1 pageNum := 1
loc, _ := time.LoadLocation("Local")
for { for {
var resp Files var resp Files
_, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) { _, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) {
@ -200,7 +199,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
break break
} }
for _, folder := range resp.FileListAO.FolderList { for _, folder := range resp.FileListAO.FolderList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", folder.LastOpTime, loc) lastOpTime := utils.MustParseCNTime(folder.LastOpTime)
res = append(res, &model.Object{ res = append(res, &model.Object{
ID: strconv.FormatInt(folder.Id, 10), ID: strconv.FormatInt(folder.Id, 10),
Name: folder.Name, Name: folder.Name,
@ -209,7 +208,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
}) })
} }
for _, file := range resp.FileListAO.FileList { for _, file := range resp.FileListAO.FileList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc) lastOpTime := utils.MustParseCNTime(file.LastOpTime)
res = append(res, &model.ObjThumb{ res = append(res, &model.ObjThumb{
Object: model.Object{ Object: model.Object{
ID: strconv.FormatInt(file.Id, 10), ID: strconv.FormatInt(file.Id, 10),

126
drivers/alist_v2/driver.go Normal file
View File

@ -0,0 +1,126 @@
package alist_v2
import (
"context"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
)
type AListV2 struct {
model.Storage
Addition
}
func (d *AListV2) Config() driver.Config {
return config
}
func (d *AListV2) GetAddition() driver.Additional {
return d.Addition
}
func (d *AListV2) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
return err
}
func (d *AListV2) Drop(ctx context.Context) error {
return nil
}
func (d *AListV2) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: dir.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
var files []model.Obj
for _, f := range resp.Data.Files {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: *f.UpdatedAt,
Size: f.Size,
IsFolder: f.Type == 1,
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
files = append(files, &file)
}
return files, nil
}
//func (d *AList) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AListV2) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: file.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Data.Files[0].Url,
}, nil
}
func (d *AListV2) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement
}
func (d *AListV2) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotImplement
}
func (d *AListV2) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotImplement
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*AListV2)(nil)

26
drivers/alist_v2/meta.go Normal file
View File

@ -0,0 +1,26 @@
package alist_v2
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
}
var config = driver.Config{
Name: "AList V2",
LocalSort: true,
NoUpload: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &AListV2{}
})
}

31
drivers/alist_v2/types.go Normal file
View File

@ -0,0 +1,31 @@
package alist_v2
import (
"time"
)
type File struct {
Id string `json:"-"`
Name string `json:"name"`
Size int64 `json:"size"`
Type int `json:"type"`
Driver string `json:"driver"`
UpdatedAt *time.Time `json:"updated_at"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
SizeStr string `json:"size_str"`
TimeStr string `json:"time_str"`
}
type PathResp struct {
Type string `json:"type"`
//Meta Meta `json:"meta"`
Files []File `json:"files"`
}
type PathReq struct {
PageNum int `json:"page_num"`
PageSize int `json:"page_size"`
Password string `json:"password"`
Path string `json:"path"`
}

1
drivers/alist_v2/util.go Normal file
View File

@ -0,0 +1 @@
package alist_v2

View File

@ -6,6 +6,7 @@ import (
_ "github.com/alist-org/alist/v3/drivers/139" _ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189" _ "github.com/alist-org/alist/v3/drivers/189"
_ "github.com/alist-org/alist/v3/drivers/189pc" _ "github.com/alist-org/alist/v3/drivers/189pc"
_ "github.com/alist-org/alist/v3/drivers/alist_v2"
_ "github.com/alist-org/alist/v3/drivers/alist_v3" _ "github.com/alist-org/alist/v3/drivers/alist_v3"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive" _ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share" _ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"

View File

@ -11,7 +11,7 @@ var NoRedirectClient *resty.Client
var RestyClient = NewRestyClient() var RestyClient = NewRestyClient()
var HttpClient = &http.Client{} var HttpClient = &http.Client{}
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36" var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
var DefaultTimeout = time.Second * 10 var DefaultTimeout = time.Second * 30
func init() { func init() {
NoRedirectClient = resty.New().SetRedirectPolicy( NoRedirectClient = resty.New().SetRedirectPolicy(

View File

@ -4,11 +4,14 @@ import (
"context" "context"
"fmt" "fmt"
"net/http" "net/http"
stdpath "path"
"strconv"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils" "github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
) )
@ -33,6 +36,9 @@ func (d *GoogleDrive) Init(ctx context.Context, storage model.Storage) error {
if err != nil { if err != nil {
return err return err
} }
if d.ChunkSize == 0 {
d.ChunkSize = 5
}
return d.refreshToken() return d.refreshToken()
} }
@ -112,15 +118,37 @@ func (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error {
} }
func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
data := base.Json{ obj, _ := op.Get(ctx, d, stdpath.Join(dstDir.GetPath(), stream.GetName()))
"name": stream.GetName(),
"parents": []string{dstDir.GetID()}, var (
e Error
url string
data base.Json
res *resty.Response
err error
)
if obj != nil {
url = fmt.Sprintf("https://www.googleapis.com/upload/drive/v3/files/%s?uploadType=resumable&supportsAllDrives=true", obj.GetID())
data = base.Json{}
} else {
data = base.Json{
"name": stream.GetName(),
"parents": []string{dstDir.GetID()},
}
url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
}
req := base.NoRedirectClient.R().
SetHeaders(map[string]string{
"Authorization": "Bearer " + d.AccessToken,
"X-Upload-Content-Type": stream.GetMimetype(),
"X-Upload-Content-Length": strconv.FormatInt(stream.GetSize(), 10),
}).
SetError(&e).SetBody(data)
if obj != nil {
res, err = req.Patch(url)
} else {
res, err = req.Post(url)
} }
var e Error
url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+d.AccessToken).
SetError(&e).SetBody(data).
Post(url)
if err != nil { if err != nil {
return err return err
} }
@ -135,9 +163,13 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors) return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
} }
putUrl := res.Header().Get("location") putUrl := res.Header().Get("location")
_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) { if stream.GetSize() < d.ChunkSize*1024*1024 {
req.SetBody(stream.GetReadCloser()) _, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {
}, nil) req.SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).SetBody(stream.GetReadCloser())
}, nil)
} else {
err = d.chunkUpload(ctx, stream, putUrl)
}
return err return err
} }

View File

@ -12,6 +12,7 @@ type Addition struct {
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"` OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"` ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"`
ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"` ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"`
ChunkSize int64 `json:"chunk_size" default:"5" help:"chunk size while uploading (unit: MB)"`
} }
var config = driver.Config{ var config = driver.Config{

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
log "github.com/sirupsen/logrus"
) )
type TokenError struct { type TokenError struct {
@ -18,17 +19,22 @@ type Files struct {
} }
type File struct { type File struct {
Id string `json:"id"` Id string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
MimeType string `json:"mimeType"` MimeType string `json:"mimeType"`
ModifiedTime time.Time `json:"modifiedTime"` ModifiedTime time.Time `json:"modifiedTime"`
Size string `json:"size"` Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"` ThumbnailLink string `json:"thumbnailLink"`
ShortcutDetails struct {
TargetId string `json:"targetId"`
TargetMimeType string `json:"targetMimeType"`
} `json:"shortcutDetails"`
} }
func fileToObj(f File) *model.ObjThumb { func fileToObj(f File) *model.ObjThumb {
log.Debugf("google file: %+v", f)
size, _ := strconv.ParseInt(f.Size, 10, 64) size, _ := strconv.ParseInt(f.Size, 10, 64)
return &model.ObjThumb{ obj := &model.ObjThumb{
Object: model.Object{ Object: model.Object{
ID: f.Id, ID: f.Id,
Name: f.Name, Name: f.Name,
@ -38,6 +44,11 @@ func fileToObj(f File) *model.ObjThumb {
}, },
Thumbnail: model.Thumbnail{}, Thumbnail: model.Thumbnail{},
} }
if f.MimeType == "application/vnd.google-apps.shortcut" {
obj.ID = f.ShortcutDetails.TargetId
obj.IsFolder = f.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder"
}
return obj
} }
type Error struct { type Error struct {

View File

@ -1,10 +1,14 @@
package google_drive package google_drive
import ( import (
"context"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/go-resty/resty/v2" "github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -77,7 +81,7 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
} }
query := map[string]string{ query := map[string]string{
"orderBy": orderBy, "orderBy": orderBy,
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken", "fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink,shortcutDetails),nextPageToken",
"pageSize": "1000", "pageSize": "1000",
"q": fmt.Sprintf("'%s' in parents and trashed = false", id), "q": fmt.Sprintf("'%s' in parents and trashed = false", id),
//"includeItemsFromAllDrives": "true", //"includeItemsFromAllDrives": "true",
@ -95,3 +99,25 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
} }
return res, nil return res, nil
} }
func (d *GoogleDrive) chunkUpload(ctx context.Context, stream model.FileStreamer, url string) error {
var defaultChunkSize = d.ChunkSize * 1024 * 1024
var finish int64 = 0
for finish < stream.GetSize() {
chunkSize := stream.GetSize() - finish
if chunkSize > defaultChunkSize {
chunkSize = defaultChunkSize
}
_, err := d.request(url, http.MethodPut, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Content-Length": strconv.FormatInt(chunkSize, 10),
"Content-Range": fmt.Sprintf("bytes %d-%d/%d", finish, finish+chunkSize-1, stream.GetSize()),
}).SetBody(io.LimitReader(stream.GetReadCloser(), chunkSize))
}, nil)
if err != nil {
return err
}
finish += chunkSize
}
return nil
}

View File

@ -77,12 +77,17 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
thumb = utils.EncodePath(thumb, true) thumb = utils.EncodePath(thumb, true)
thumb += "?type=thumb" thumb += "?type=thumb"
} }
isFolder := f.IsDir() || isSymlinkDir(f, fullPath)
size := f.Size()
if isFolder {
size = 0
}
file := model.ObjThumb{ file := model.ObjThumb{
Object: model.Object{ Object: model.Object{
Name: f.Name(), Name: f.Name(),
Modified: f.ModTime(), Modified: f.ModTime(),
Size: f.Size(), Size: size,
IsFolder: f.IsDir(), IsFolder: isFolder,
}, },
Thumbnail: model.Thumbnail{ Thumbnail: model.Thumbnail{
Thumbnail: thumb, Thumbnail: thumb,
@ -101,12 +106,17 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
} }
return nil, err return nil, err
} }
isFolder := f.IsDir() || isSymlinkDir(f, path)
size := f.Size()
if isFolder {
size = 0
}
file := model.Object{ file := model.Object{
Path: path, Path: path,
Name: f.Name(), Name: f.Name(),
Modified: f.ModTime(), Modified: f.ModTime(),
Size: f.Size(), Size: size,
IsFolder: f.IsDir(), IsFolder: isFolder,
} }
return &file, nil return &file, nil
} }

View File

@ -1 +1,25 @@
package local package local
import (
"io/fs"
"os"
"path/filepath"
)
func isSymlinkDir(f fs.FileInfo, path string) bool {
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
dst, err := os.Readlink(filepath.Join(path, f.Name()))
if err != nil {
return false
}
if !filepath.IsAbs(dst) {
dst = filepath.Join(path, dst)
}
stat, err := os.Stat(dst)
if err != nil {
return false
}
return stat.IsDir()
}
return false
}

3
go.mod
View File

@ -3,7 +3,7 @@ module github.com/alist-org/alist/v3
go 1.19 go 1.19
require ( require (
github.com/SheltonZhu/115driver v1.0.9 github.com/SheltonZhu/115driver v1.0.12
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
github.com/aws/aws-sdk-go v1.44.88 github.com/aws/aws-sdk-go v1.44.88
github.com/caarlos0/env/v6 v6.9.3 github.com/caarlos0/env/v6 v6.9.3
@ -71,6 +71,7 @@ require (
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 // indirect github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect

6
go.sum
View File

@ -2,8 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/SheltonZhu/115driver v1.0.9 h1:QlSgsrbmwiFB74L3z5NCsHBjAFKlwmOShGiD9qinXBc= github.com/SheltonZhu/115driver v1.0.12 h1:+GlIM5h8tzuec6MzK0wFwb7bY77nav7JhY8lTljzls4=
github.com/SheltonZhu/115driver v1.0.9/go.mod h1:Xv7lKJ3BXyc+vU5YfymXHG8e/QTopRawSHwk09K7RRw= github.com/SheltonZhu/115driver v1.0.12/go.mod h1:00ixivHH5HqDj4S7kAWbkuUrjtsJTxc7cGv5RMw3RVs=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE= github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04= github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ= github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
@ -221,6 +221,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=

View File

@ -105,7 +105,14 @@ func (m *Monitor) Update() (bool, error) {
return true, errors.WithMessage(err, "failed to transfer file") return true, errors.WithMessage(err, "failed to transfer file")
case "error": case "error":
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.ErrorMessage) return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.ErrorMessage)
case "active", "waiting", "paused": case "active":
m.tsk.SetStatus("aria2: " + info.Status)
if info.Seeder == "true" {
err := m.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
}
return false, nil
case "waiting", "paused":
m.tsk.SetStatus("aria2: " + info.Status) m.tsk.SetStatus("aria2: " + info.Status)
return false, nil return false, nil
case "removed": case "removed":

9
pkg/utils/email.go Normal file
View File

@ -0,0 +1,9 @@
package utils
import "regexp"
func IsEmailFormat(email string) bool {
pattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\.){1,4}[a-z]{2,4}$`
reg := regexp.MustCompile(pattern)
return reg.MatchString(email)
}

8
pkg/utils/time.go Normal file
View File

@ -0,0 +1,8 @@
package utils
import "time"
func MustParseCNTime(str string) time.Time {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
return lastOpTime
}

View File

@ -3,11 +3,12 @@ package common
import ( import (
"github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/sign" "github.com/alist-org/alist/v3/internal/sign"
stdpath "path"
) )
func Sign(obj model.Obj, encrypt bool) string { func Sign(obj model.Obj, parent string, encrypt bool) string {
if obj.IsDir() || !encrypt { if obj.IsDir() || !encrypt {
return "" return ""
} }
return sign.Sign(obj.GetName()) return sign.Sign(stdpath.Join(parent, obj.GetName()))
} }

View File

@ -37,6 +37,7 @@ func Down(c *gin.Context) {
return return
} }
c.Header("Referrer-Policy", "no-referrer") c.Header("Referrer-Policy", "no-referrer")
c.Header("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
c.Redirect(302, link.URL) c.Redirect(302, link.URL)
} }
} }
@ -57,7 +58,7 @@ func Proxy(c *gin.Context) {
URL := fmt.Sprintf("%s%s?sign=%s", URL := fmt.Sprintf("%s%s?sign=%s",
strings.Split(downProxyUrl, "\n")[0], strings.Split(downProxyUrl, "\n")[0],
utils.EncodePath(rawPath, true), utils.EncodePath(rawPath, true),
sign.Sign(filename)) sign.Sign(rawPath))
c.Redirect(302, URL) c.Redirect(302, URL)
return return
} }

View File

@ -203,8 +203,8 @@ func Link(c *gin.Context) {
common.SuccessResp(c, model.Link{ common.SuccessResp(c, model.Link{
URL: fmt.Sprintf("%s/p%s?d&sign=%s", URL: fmt.Sprintf("%s/p%s?d&sign=%s",
common.GetApiUrl(c.Request), common.GetApiUrl(c.Request),
utils.EncodePath(req.Path, true), utils.EncodePath(rawPath, true),
sign.Sign(stdpath.Base(rawPath))), sign.Sign(rawPath)),
}) })
return return
} }

View File

@ -86,7 +86,7 @@ func FsList(c *gin.Context) {
provider = storage.GetStorage().Driver provider = storage.GetStorage().Driver
} }
common.SuccessResp(c, FsListResp{ common.SuccessResp(c, FsListResp{
Content: toObjResp(objs, isEncrypt(meta, req.Path)), Content: toObjResp(objs, req.Path, isEncrypt(meta, req.Path)),
Total: int64(total), Total: int64(total),
Readme: getReadme(meta, req.Path), Readme: getReadme(meta, req.Path),
Write: user.CanWrite() || canWrite(meta, req.Path), Write: user.CanWrite() || canWrite(meta, req.Path),
@ -196,7 +196,7 @@ func pagination(objs []model.Obj, req *common.PageReq) (int, []model.Obj) {
return total, objs[start:end] return total, objs[start:end]
} }
func toObjResp(objs []model.Obj, encrypt bool) []ObjResp { func toObjResp(objs []model.Obj, parent string, encrypt bool) []ObjResp {
var resp []ObjResp var resp []ObjResp
for _, obj := range objs { for _, obj := range objs {
thumb := "" thumb := ""
@ -212,7 +212,7 @@ func toObjResp(objs []model.Obj, encrypt bool) []ObjResp {
Size: obj.GetSize(), Size: obj.GetSize(),
IsDir: obj.IsDir(), IsDir: obj.IsDir(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
Sign: common.Sign(obj, encrypt), Sign: common.Sign(obj, parent, encrypt),
Thumb: thumb, Thumb: thumb,
Type: tp, Type: tp,
}) })
@ -272,12 +272,15 @@ func FsGet(c *gin.Context) {
} }
if storage.Config().MustProxy() || storage.GetStorage().WebProxy { if storage.Config().MustProxy() || storage.GetStorage().WebProxy {
if storage.GetStorage().DownProxyUrl != "" { if storage.GetStorage().DownProxyUrl != "" {
rawURL = fmt.Sprintf("%s%s?sign=%s", strings.Split(storage.GetStorage().DownProxyUrl, "\n")[0], req.Path, sign.Sign(obj.GetName())) rawURL = fmt.Sprintf("%s%s?sign=%s",
strings.Split(storage.GetStorage().DownProxyUrl, "\n")[0],
utils.EncodePath(req.Path, true),
sign.Sign(req.Path))
} else { } else {
rawURL = fmt.Sprintf("%s/p%s?sign=%s", rawURL = fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(c.Request), common.GetApiUrl(c.Request),
utils.EncodePath(req.Path, true), utils.EncodePath(req.Path, true),
sign.Sign(obj.GetName())) sign.Sign(req.Path))
} }
} else { } else {
// file have raw url // file have raw url
@ -307,13 +310,13 @@ func FsGet(c *gin.Context) {
Size: obj.GetSize(), Size: obj.GetSize(),
IsDir: obj.IsDir(), IsDir: obj.IsDir(),
Modified: obj.ModTime(), Modified: obj.ModTime(),
Sign: common.Sign(obj, isEncrypt(meta, req.Path)), Sign: common.Sign(obj, parentPath, isEncrypt(meta, req.Path)),
Type: utils.GetFileType(obj.GetName()), Type: utils.GetFileType(obj.GetName()),
}, },
RawURL: rawURL, RawURL: rawURL,
Readme: getReadme(meta, req.Path), Readme: getReadme(meta, req.Path),
Provider: provider, Provider: provider,
Related: toObjResp(related, isEncrypt(parentMeta, parentPath)), Related: toObjResp(related, parentPath, isEncrypt(parentMeta, parentPath)),
}) })
} }

View File

@ -1,7 +1,6 @@
package middlewares package middlewares
import ( import (
stdpath "path"
"strings" "strings"
"github.com/alist-org/alist/v3/internal/db" "github.com/alist-org/alist/v3/internal/db"
@ -17,7 +16,6 @@ import (
func Down(c *gin.Context) { func Down(c *gin.Context) {
rawPath := parsePath(c.Param("path")) rawPath := parsePath(c.Param("path"))
c.Set("path", rawPath) c.Set("path", rawPath)
filename := stdpath.Base(rawPath)
meta, err := db.GetNearestMeta(rawPath) meta, err := db.GetNearestMeta(rawPath)
if err != nil { if err != nil {
if !errors.Is(errors.Cause(err), errs.MetaNotFound) { if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
@ -29,7 +27,7 @@ func Down(c *gin.Context) {
// verify sign // verify sign
if needSign(meta, rawPath) { if needSign(meta, rawPath) {
s := c.Query("sign") s := c.Query("sign")
err = sign.Verify(filename, strings.TrimSuffix(s, "/")) err = sign.Verify(rawPath, strings.TrimSuffix(s, "/"))
if err != nil { if err != nil {
common.ErrorResp(c, err, 401) common.ErrorResp(c, err, 401)
c.Abort() c.Abort()

View File

@ -231,7 +231,8 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
u := fmt.Sprintf("%s/p%s?sign=%s", u := fmt.Sprintf("%s/p%s?sign=%s",
common.GetApiUrl(r), common.GetApiUrl(r),
utils.EncodePath(reqPath, true), utils.EncodePath(reqPath, true),
sign.Sign(path.Base(reqPath))) sign.Sign(reqPath))
w.Header().Set("Cache-Control", "max-age=0, no-cache, no-store, must-revalidate")
http.Redirect(w, r, u, 302) http.Redirect(w, r, u, 302)
} else { } else {
link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r)}) link, _, err := fs.Link(ctx, reqPath, model.LinkArgs{IP: utils.ClientIP(r)})