diff --git a/drivers/189pc/utils.go b/drivers/189pc/utils.go index 6c559c1e..47a09c21 100644 --- a/drivers/189pc/utils.go +++ b/drivers/189pc/utils.go @@ -533,7 +533,9 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model. silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte))) silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte))) } - file.GetReadCloser().(*os.File).Seek(0, io.SeekStart) + if _, err = tempFile.Seek(0, io.SeekStart); err != nil { + return err + } fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil))) sliceMd5Hex := fileMd5Hex diff --git a/drivers/all.go b/drivers/all.go index 37fbf927..53b311df 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -7,6 +7,7 @@ import ( _ "github.com/alist-org/alist/v3/drivers/189pc" _ "github.com/alist-org/alist/v3/drivers/aliyundrive" _ "github.com/alist-org/alist/v3/drivers/baidu_netdisk" + _ "github.com/alist-org/alist/v3/drivers/baidu_photo" _ "github.com/alist-org/alist/v3/drivers/ftp" _ "github.com/alist-org/alist/v3/drivers/google_drive" _ "github.com/alist-org/alist/v3/drivers/local" diff --git a/drivers/baidu_photo/driver.go b/drivers/baidu_photo/driver.go new file mode 100644 index 00000000..ef264084 --- /dev/null +++ b/drivers/baidu_photo/driver.go @@ -0,0 +1,277 @@ +package baiduphoto + +import ( + "context" + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "math" + "os" + "regexp" + + "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/go-resty/resty/v2" +) + +type BaiduPhoto struct { + model.Storage + Addition + + AccessToken string +} + +func (d *BaiduPhoto) Config() driver.Config { + return config +} + +func (d *BaiduPhoto) GetAddition() driver.Additional { + return d.Addition +} + +func (d *BaiduPhoto) 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 + } + return d.refreshToken() +} + +func (d *BaiduPhoto) Drop(ctx context.Context) error { + return nil +} + +func (d *BaiduPhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + var objs []model.Obj + var err error + if IsRoot(dir) { + var albums []Album + if d.ShowType != "root_only_file" { + albums, err = d.GetAllAlbum(ctx) + if err != nil { + return nil, err + } + } + + var files []File + if d.ShowType != "root_only_album" { + files, err = d.GetAllFile(ctx) + if err != nil { + return nil, err + } + } + + objs = make([]model.Obj, 0, len(files)+len(albums)) + for i := 0; i < len(albums); i++ { + objs = append(objs, &albums[i]) + } + for i := 0; i < len(files); i++ { + objs = append(objs, &files[i]) + } + } else if IsAlbum(dir) || IsAlbumRoot(dir) { + var files []AlbumFile + files, err = d.GetAllAlbumFile(ctx, splitID(dir.GetID())[0], "") + if err != nil { + return nil, err + } + objs = make([]model.Obj, 0, len(files)) + for i := 0; i < len(files); i++ { + objs = append(objs, &files[i]) + } + } + return objs, nil +} + +func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + if IsAlbumFile(file) { + return d.linkAlbum(ctx, file, args) + } else if IsFile(file) { + return d.linkFile(ctx, file, args) + } + return nil, errs.NotFile +} + +func (d *BaiduPhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + if IsRoot(parentDir) { + code := regexp.MustCompile(`(?i)join:([\S]*)`).FindStringSubmatch(dirName) + if len(code) > 1 { + return d.JoinAlbum(ctx, code[1]) + } + return d.CreateAlbum(ctx, dirName) + } + return errs.NotSupport +} + +func (d *BaiduPhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + if IsFile(srcObj) { + if IsAlbum(dstDir) { + //rootfile -> album + e := splitID(dstDir.GetID()) + return d.AddAlbumFile(ctx, e[0], e[1], srcObj.GetID()) + } + } else if IsAlbumFile(srcObj) { + if IsRoot(dstDir) { + //albumfile -> root + e := splitID(srcObj.GetID()) + _, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID()) + return err + } else if IsAlbum(dstDir) { + // albumfile -> root -> album + e := splitID(srcObj.GetID()) + file, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID()) + if err != nil { + return err + } + e = splitID(dstDir.GetID()) + return d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(file.Fsid)) + } + } + return errs.NotSupport +} + +func (d *BaiduPhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + // 仅支持相册之间移动 + if IsAlbumFile(srcObj) && IsAlbum(dstDir) { + err := d.Copy(ctx, srcObj, dstDir) + if err != nil { + return err + } + e := splitID(srcObj.GetID()) + return d.DeleteAlbumFile(ctx, e[1], e[2], srcObj.GetID()) + } + return errs.NotSupport +} + +func (d *BaiduPhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + // 仅支持相册改名 + if IsAlbum(srcObj) { + e := splitID(srcObj.GetID()) + return d.SetAlbumName(ctx, e[0], e[1], newName) + } + return errs.NotSupport +} + +func (d *BaiduPhoto) Remove(ctx context.Context, obj model.Obj) error { + e := splitID(obj.GetID()) + if IsFile(obj) { + return d.DeleteFile(ctx, e[0]) + } else if IsAlbum(obj) { + return d.DeleteAlbum(ctx, e[0], e[1]) + } else if IsAlbumFile(obj) { + return d.DeleteAlbumFile(ctx, e[1], e[2], obj.GetID()) + } + return errs.NotSupport +} + +func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + // 需要获取完整文件md5,必须支持 io.Seek + tempFile, err := utils.CreateTempFile(stream.GetReadCloser()) + if err != nil { + return err + } + defer func() { + _ = tempFile.Close() + _ = os.Remove(tempFile.Name()) + }() + stream.SetReadCloser(tempFile) + + // 计算需要的数据 + const DEFAULT = 1 << 22 + const SliceSize = 1 << 18 + count := int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT))) + + sliceMD5List := make([]string, 0, count) + fileMd5 := md5.New() + sliceMd5 := md5.New() + sliceMd52 := md5.New() + slicemd52Write := utils.LimitWriter(sliceMd52, SliceSize) + for i := 1; i <= count; i++ { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + _, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, slicemd52Write), stream, DEFAULT) + if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF { + return err + } + sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil))) + sliceMd5.Reset() + } + if _, err = tempFile.Seek(0, io.SeekStart); err != nil { + return err + } + content_md5 := hex.EncodeToString(fileMd5.Sum(nil)) + slice_md5 := hex.EncodeToString(sliceMd52.Sum(nil)) + + // 开始执行上传 + params := map[string]string{ + "autoinit": "1", + "isdir": "0", + "rtype": "1", + "ctype": "11", + "path": stream.GetName(), + "size": fmt.Sprint(stream.GetSize()), + "slice-md5": slice_md5, + "content-md5": content_md5, + "block_list": MustString(utils.Json.MarshalToString(sliceMD5List)), + } + + // 预上传 + var precreateResp PrecreateResp + _, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(params) + }, &precreateResp) + if err != nil { + return err + } + + switch precreateResp.ReturnType { + case 1: // 上传文件 + uploadParams := map[string]string{ + "method": "upload", + "path": params["path"], + "uploadid": precreateResp.UploadID, + } + + for i := 0; i < count; i++ { + uploadParams["partseq"] = fmt.Sprint(i) + _, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(uploadParams) + r.SetFileReader("file", stream.GetName(), io.LimitReader(tempFile, DEFAULT)) + }, nil) + if err != nil { + return err + } + up(i * 100 / count) + } + fallthrough + case 2: // 创建文件 + params["uploadid"] = precreateResp.UploadID + _, err = d.Post(FILE_API_URL_V1+"/create", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(params) + }, &precreateResp) + if err != nil { + return err + } + fallthrough + case 3: // 增加到相册 + if IsAlbum(dstDir) || IsAlbumRoot(dstDir) { + e := splitID(dstDir.GetID()) + err = d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(precreateResp.Data.FsID)) + if err != nil { + return err + } + } + } + return nil +} + +var _ driver.Driver = (*BaiduPhoto)(nil) diff --git a/drivers/baidu_photo/help.go b/drivers/baidu_photo/help.go new file mode 100644 index 00000000..80c8e673 --- /dev/null +++ b/drivers/baidu_photo/help.go @@ -0,0 +1,107 @@ +package baiduphoto + +import ( + "fmt" + "math" + "math/rand" + "regexp" + "strings" + "time" + + "github.com/alist-org/alist/v3/internal/model" +) + +//Tid生成 +func getTid() string { + return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000)) +} + +// 检查名称 +func checkName(name string) bool { + return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_-]").MatchString(name) +} + +func toTime(t int64) *time.Time { + tm := time.Unix(t, 0) + return &tm +} + +func fsidsFormat(ids ...string) string { + var buf []string + for _, id := range ids { + e := splitID(id) + buf = append(buf, fmt.Sprintf(`{"fsid":%s,"uk":%s}`, e[0], e[3])) + } + return fmt.Sprintf("[%s]", strings.Join(buf, ",")) +} + +func fsidsFormatNotUk(ids ...string) string { + var buf []string + for _, id := range ids { + buf = append(buf, fmt.Sprintf(`{"fsid":%s}`, splitID(id)[0])) + } + return fmt.Sprintf("[%s]", strings.Join(buf, ",")) +} + +/* +结构 + +{fsid} 文件 + +{album_id}|{tid} 相册 + +{fsid}|{album_id}|{tid}|{uk} 相册文件 +*/ +func splitID(id string) []string { + return strings.SplitN(id, "|", 4)[:4] +} + +/* +结构 + +{fsid} 文件 + +{album_id}|{tid} 相册 + +{fsid}|{album_id}|{tid}|{uk} 相册文件 +*/ +func joinID(ids ...interface{}) string { + idsStr := make([]string, 0, len(ids)) + for _, id := range ids { + idsStr = append(idsStr, fmt.Sprint(id)) + } + return strings.Join(idsStr, "|") +} + +func getFileName(path string) string { + return path[strings.LastIndex(path, "/")+1:] +} + +// 相册 +func IsAlbum(obj model.Obj) bool { + return obj.IsDir() && obj.GetPath() == "album" +} + +// 根目录 +func IsRoot(obj model.Obj) bool { + return obj.IsDir() && obj.GetPath() == "" && obj.GetID() == "" +} + +// 以相册为根目录 +func IsAlbumRoot(obj model.Obj) bool { + return obj.IsDir() && obj.GetPath() == "" && obj.GetID() != "" +} + +// 根文件 +func IsFile(obj model.Obj) bool { + return !obj.IsDir() && obj.GetPath() == "file" +} + +// 相册文件 +func IsAlbumFile(obj model.Obj) bool { + return !obj.IsDir() && obj.GetPath() == "albumfile" +} + +func MustString(str string, err error) string { + return str +} diff --git a/drivers/baidu_photo/meta.go b/drivers/baidu_photo/meta.go new file mode 100644 index 00000000..a2364c37 --- /dev/null +++ b/drivers/baidu_photo/meta.go @@ -0,0 +1,30 @@ +package baiduphoto + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + RefreshToken string `json:"refresh_token" required:"true"` + ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"` + AlbumID string `json:"album_id"` + //AlbumPassword string `json:"album_password"` + ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"` + ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"` +} + +func (a Addition) GetRootId() string { + return a.AlbumID +} + +var config = driver.Config{ + Name: "BaiduPhoto", + LocalSort: true, +} + +func init() { + op.RegisterDriver(config, func() driver.Driver { + return &BaiduPhoto{} + }) +} diff --git a/drivers/baidu_photo/types.go b/drivers/baidu_photo/types.go new file mode 100644 index 00000000..81499cfd --- /dev/null +++ b/drivers/baidu_photo/types.go @@ -0,0 +1,169 @@ +package baiduphoto + +import ( + "fmt" + "time" +) + +type TokenErrResp struct { + ErrorDescription string `json:"error_description"` + ErrorMsg string `json:"error"` +} + +func (e *TokenErrResp) Error() string { + return fmt.Sprint(e.ErrorMsg, " : ", e.ErrorDescription) +} + +type Erron struct { + Errno int `json:"errno"` + RequestID int `json:"request_id"` +} + +type Page struct { + HasMore int `json:"has_more"` + Cursor string `json:"cursor"` +} + +func (p Page) HasNextPage() bool { + return p.HasMore == 1 +} + +type ( + FileListResp struct { + Page + List []File `json:"list"` + } + + File struct { + Fsid int64 `json:"fsid"` // 文件ID + Path string `json:"path"` // 文件路径 + Size int64 `json:"size"` + Ctime int64 `json:"ctime"` // 创建时间 s + Mtime int64 `json:"mtime"` // 修改时间 s + Thumburl []string `json:"thumburl"` + + parseTime *time.Time + } +) + +func (c *File) GetSize() int64 { return c.Size } +func (c *File) GetName() string { return getFileName(c.Path) } +func (c *File) ModTime() time.Time { + if c.parseTime == nil { + c.parseTime = toTime(c.Mtime) + } + return *c.parseTime +} +func (c *File) IsDir() bool { return false } +func (c *File) GetID() string { return joinID(c.Fsid) } +func (c *File) GetPath() string { return "file" } +func (c *File) Thumb() string { + if len(c.Thumburl) > 0 { + return c.Thumburl[0] + } + return "" +} + +/*相册部分*/ +type ( + AlbumListResp struct { + Page + List []Album `json:"list"` + Reset int64 `json:"reset"` + TotalCount int64 `json:"total_count"` + } + + Album struct { + AlbumID string `json:"album_id"` + Tid int64 `json:"tid"` + Title string `json:"title"` + JoinTime int64 `json:"join_time"` + CreateTime int64 `json:"create_time"` + Mtime int64 `json:"mtime"` + + parseTime *time.Time + } + + AlbumFileListResp struct { + Page + List []AlbumFile `json:"list"` + Reset int64 `json:"reset"` + TotalCount int64 `json:"total_count"` + } + + AlbumFile struct { + File + AlbumID string `json:"album_id"` + Tid int64 `json:"tid"` + Uk int64 `json:"uk"` + } +) + +func (a *Album) GetSize() int64 { return 0 } +func (a *Album) GetName() string { return fmt.Sprint(a.Title, " -> ", a.AlbumID) } +func (a *Album) ModTime() time.Time { + if a.parseTime == nil { + a.parseTime = toTime(a.Mtime) + } + return *a.parseTime +} +func (a *Album) IsDir() bool { return true } +func (a *Album) GetID() string { return joinID(a.AlbumID, a.Tid) } +func (a *Album) GetPath() string { return "album" } + +func (af *AlbumFile) GetID() string { return joinID(af.Fsid, af.AlbumID, af.Tid, af.Uk) } +func (c *AlbumFile) GetPath() string { return "albumfile" } + +type ( + CopyFileResp struct { + List []CopyFile `json:"list"` + } + CopyFile struct { + FromFsid int64 `json:"from_fsid"` // 源ID + Fsid int64 `json:"fsid"` // 目标ID + Path string `json:"path"` + ShootTime int `json:"shoot_time"` + } +) + +/*上传部分*/ +type ( + UploadFile struct { + FsID int64 `json:"fs_id"` + Size int64 `json:"size"` + Md5 string `json:"md5"` + ServerFilename string `json:"server_filename"` + Path string `json:"path"` + Ctime int `json:"ctime"` + Mtime int `json:"mtime"` + Isdir int `json:"isdir"` + Category int `json:"category"` + ServerMd5 string `json:"server_md5"` + ShootTime int `json:"shoot_time"` + } + + CreateFileResp struct { + Data UploadFile `json:"data"` + } + + PrecreateResp struct { + ReturnType int `json:"return_type"` //存在返回2 不存在返回1 已经保存3 + //存在返回 + CreateFileResp + + //不存在返回 + Path string `json:"path"` + UploadID string `json:"uploadid"` + Blocklist []int64 `json:"block_list"` + } +) + +type InviteResp struct { + Pdata struct { + // 邀请码 + InviteCode string `json:"invite_code"` + // 有效时间 + ExpireTime int `json:"expire_time"` + ShareID string `json:"share_id"` + } `json:"pdata"` +} diff --git a/drivers/baidu_photo/utils.go b/drivers/baidu_photo/utils.go new file mode 100644 index 00000000..c4bd216d --- /dev/null +++ b/drivers/baidu_photo/utils.go @@ -0,0 +1,377 @@ +package baiduphoto + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/errs" + "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/go-resty/resty/v2" +) + +const ( + API_URL = "https://photo.baidu.com/youai" + ALBUM_API_URL = API_URL + "/album/v1" + FILE_API_URL_V1 = API_URL + "/file/v1" + FILE_API_URL_V2 = API_URL + "/file/v2" +) + +var ( + ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20") +) + +func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + req := base.RestyClient.R(). + SetQueryParam("access_token", p.AccessToken) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + res, err := req.Execute(method, furl) + if err != nil { + return nil, err + } + + erron := utils.Json.Get(res.Body(), "errno").ToInt() + switch erron { + case 0: + break + case 50805: + return nil, fmt.Errorf("you have joined album") + case 50820: + return nil, fmt.Errorf("no shared albums found") + case -6: + if err = p.refreshToken(); err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron) + } + return res.Body(), nil +} + +func (p *BaiduPhoto) refreshToken() error { + u := "https://openapi.baidu.com/oauth/2.0/token" + var resp base.TokenResp + var e TokenErrResp + _, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{ + "grant_type": "refresh_token", + "refresh_token": p.RefreshToken, + "client_id": p.ClientID, + "client_secret": p.ClientSecret, + }).Get(u) + if err != nil { + return err + } + if e.ErrorMsg != "" { + return &e + } + if resp.RefreshToken == "" { + return errs.EmptyToken + } + p.AccessToken, p.RefreshToken = resp.AccessToken, resp.RefreshToken + op.MustSaveDriverStorage(p) + return nil +} + +func (p *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + return p.Request(furl, http.MethodGet, callback, resp) +} + +func (p *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + return p.Request(furl, http.MethodPost, callback, resp) +} + +// 获取所有文件 +func (p *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) { + var cursor string + for { + var resp FileListResp + _, err = p.Get(FILE_API_URL_V1+"/list", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(map[string]string{ + "need_thumbnail": "1", + "need_filter_hidden": "0", + "cursor": cursor, + }) + }, &resp) + if err != nil { + return + } + + files = append(files, resp.List...) + if !resp.HasNextPage() { + return + } + cursor = resp.Cursor + } +} + +// 删除根文件 +func (p *BaiduPhoto) DeleteFile(ctx context.Context, fileIDs ...string) error { + _, err := p.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) { + req.SetContext(ctx) + req.SetQueryParams(map[string]string{ + "fsid_list": fmt.Sprintf("[%s]", strings.Join(fileIDs, ",")), + }) + }, nil) + return err +} + +// 获取所有相册 +func (p *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) { + var cursor string + for { + var resp AlbumListResp + _, err = p.Get(ALBUM_API_URL+"/list", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(map[string]string{ + "need_amount": "1", + "limit": "100", + "cursor": cursor, + }) + }, &resp) + if err != nil { + return + } + if albums == nil { + albums = make([]Album, 0, resp.TotalCount) + } + + cursor = resp.Cursor + albums = append(albums, resp.List...) + + if !resp.HasNextPage() { + return + } + } +} + +// 获取相册中所有文件 +func (p *BaiduPhoto) GetAllAlbumFile(ctx context.Context, albumID, passwd string) (files []AlbumFile, err error) { + var cursor string + for { + var resp AlbumFileListResp + _, err = p.Get(ALBUM_API_URL+"/listfile", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(map[string]string{ + "album_id": albumID, + "need_amount": "1", + "limit": "1000", + "passwd": passwd, + "cursor": cursor, + }) + }, &resp) + if err != nil { + return + } + if files == nil { + files = make([]AlbumFile, 0, resp.TotalCount) + } + + cursor = resp.Cursor + files = append(files, resp.List...) + + if !resp.HasNextPage() { + return + } + } +} + +// 创建相册 +func (p *BaiduPhoto) CreateAlbum(ctx context.Context, name string) error { + if !checkName(name) { + return ErrNotSupportName + } + _, err := p.Post(ALBUM_API_URL+"/create", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(map[string]string{ + "title": name, + "tid": getTid(), + "source": "0", + }) + }, nil) + return err +} + +// 相册改名 +func (p *BaiduPhoto) SetAlbumName(ctx context.Context, albumID, tID, name string) error { + if !checkName(name) { + return ErrNotSupportName + } + + _, err := p.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(map[string]string{ + "title": name, + "album_id": albumID, + "tid": tID, + }) + }, nil) + return err +} + +// 删除相册 +func (p *BaiduPhoto) DeleteAlbum(ctx context.Context, albumID, tID string) error { + _, err := p.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(map[string]string{ + "album_id": albumID, + "tid": tID, + "delete_origin_image": "0", // 是否删除原图 0 不删除 1 删除 + }) + }, nil) + return err +} + +// 删除相册文件 +func (p *BaiduPhoto) DeleteAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error { + _, err := p.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(map[string]string{ + "album_id": albumID, + "tid": tID, + "list": fsidsFormat(fileIDs...), + "del_origin": "0", // 是否删除原图 0 不删除 1 删除 + }) + }, nil) + return err +} + +// 增加相册文件 +func (p *BaiduPhoto) AddAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error { + _, err := p.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) { + r.SetContext(ctx) + r.SetQueryParams(map[string]string{ + "album_id": albumID, + "tid": tID, + "list": fsidsFormatNotUk(fileIDs...), + }) + }, nil) + return err +} + +// 保存相册文件为根文件 +func (p *BaiduPhoto) CopyAlbumFile(ctx context.Context, albumID, tID, uk string, fileID ...string) (*CopyFile, error) { + var resp CopyFileResp + _, err := p.Post(ALBUM_API_URL+"/copyfile", func(r *resty.Request) { + r.SetContext(ctx) + r.SetFormData(map[string]string{ + "album_id": albumID, + "tid": tID, + "uk": uk, + "list": fsidsFormatNotUk(fileID...), + }) + r.SetResult(&resp) + }, nil) + if err != nil { + return nil, err + } + return &resp.List[0], nil +} + +// 加入相册 +func (p *BaiduPhoto) JoinAlbum(ctx context.Context, code string) error { + var resp InviteResp + _, err := p.Get(ALBUM_API_URL+"/querypcode", func(req *resty.Request) { + req.SetContext(ctx) + req.SetQueryParams(map[string]string{ + "pcode": code, + "web": "1", + }) + }, &resp) + if err != nil { + return err + } + _, err = p.Get(ALBUM_API_URL+"/join", func(req *resty.Request) { + req.SetContext(ctx) + req.SetQueryParams(map[string]string{ + "invite_code": resp.Pdata.InviteCode, + }) + }, nil) + return err +} + +func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + headers := map[string]string{ + "User-Agent": base.UserAgent, + } + if args.Header.Get("User-Agent") != "" { + headers["User-Agent"] = args.Header.Get("User-Agent") + } + if !utils.IsLocalIPAddr(args.IP) { + headers["X-Forwarded-For"] = args.IP + } + + e := splitID(file.GetID()) + res, err := base.NoRedirectClient.R(). + SetContext(ctx). + SetHeaders(headers). + SetQueryParams(map[string]string{ + "access_token": d.AccessToken, + "fsid": e[0], + "album_id": e[1], + "tid": e[2], + "uk": e[3], + }). + Head(ALBUM_API_URL + "/download") + + if err != nil { + return nil, err + } + + exp := 8 * time.Hour + link := &model.Link{ + URL: res.Header().Get("location"), + Header: http.Header{ + "User-Agent": []string{headers["User-Agent"]}, + }, + Expiration: &exp, + } + return link, nil +} + +func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + headers := map[string]string{ + "User-Agent": base.UserAgent, + } + if args.Header.Get("User-Agent") != "" { + headers["User-Agent"] = args.Header.Get("User-Agent") + } + if !utils.IsLocalIPAddr(args.IP) { + headers["X-Forwarded-For"] = args.IP + } + + var downloadUrl struct { + Dlink string `json:"dlink"` + } + _, err := d.Get(FILE_API_URL_V2+"/download", func(r *resty.Request) { + r.SetContext(ctx) + r.SetHeaders(headers) + r.SetQueryParams(map[string]string{ + "fsid": splitID(file.GetID())[0], + }) + }, &downloadUrl) + if err != nil { + return nil, err + } + + exp := 8 * time.Hour + link := &model.Link{ + URL: downloadUrl.Dlink, + Header: http.Header{ + "User-Agent": []string{headers["User-Agent"]}, + }, + Expiration: &exp, + } + return link, nil +} diff --git a/pkg/utils/io.go b/pkg/utils/io.go index 884919c6..38450248 100644 --- a/pkg/utils/io.go +++ b/pkg/utils/io.go @@ -40,3 +40,32 @@ func CopyWithCtx(ctx context.Context, out io.Writer, in io.Reader, size int64, p })) return err } + +type limitWriter struct { + w io.Writer + count int64 + limit int64 +} + +func (l limitWriter) Write(p []byte) (n int, err error) { + wn := int(l.limit - l.count) + if wn > len(p) { + wn = len(p) + } + if wn > 0 { + if n, err = l.w.Write(p[:wn]); err != nil { + return + } + if n < wn { + err = io.ErrShortWrite + } + } + if err == nil { + n = len(p) + } + return +} + +func LimitWriter(w io.Writer, size int64) io.Writer { + return &limitWriter{w: w, limit: size} +} diff --git a/server/handles/fsread.go b/server/handles/fsread.go index 0c09cf47..4f722133 100644 --- a/server/handles/fsread.go +++ b/server/handles/fsread.go @@ -270,7 +270,7 @@ func FsGet(c *gin.Context) { } } else { // if storage is not proxy, use raw url by fs.Link - link, _, err := fs.Link(c, req.Path, model.LinkArgs{IP: c.ClientIP()}) + link, _, err := fs.Link(c, req.Path, model.LinkArgs{IP: c.ClientIP(), Header: c.Request.Header}) if err != nil { common.ErrorResp(c, err, 500) return