Compare commits

..

27 Commits

Author SHA1 Message Date
Noah Hsu b8cf02ca68 fix(aria2): retry 5 times for get status (close #1857) 2022-10-18 15:27:19 +08:00
Jake Liu 3db798a82a feat(google_photo): Add categories in root, add album support. (#2046)
* feat(google_photo): Add categories in root, add album support.

* fix(google_photo): Remove else block in `drive/google_photo/types.go:60`
2022-10-18 15:19:05 +08:00
Noah Hsu 45cc0cedbd fix(s3): mkdir and delete (close #2029) 2022-10-18 15:10:47 +08:00
foxxorcat 2efade123e fix(189pc):slice bounds out of range close #2045 2022-10-17 22:39:51 +08:00
foxxorcat fc393f743f fix(thunder):no additional processing when the deviceID is correct 2022-10-17 22:37:17 +08:00
foxxorcat 0e99e7e9b9 fix(thunder,189pc): some known problems 2022-10-17 00:54:39 +08:00
Noah Hsu 7a95850c1b fix(google_drive): incorrect ModifiedTime (close #2002) 2022-10-14 14:17:33 +08:00
Noah Hsu 549355bb29 build: change golang version 2022-10-12 17:35:44 +08:00
Noah Hsu 55aa8ee3b1 fix: version print of build script [skip ci] 2022-10-12 17:24:04 +08:00
Noah Hsu 1c22fc367e docs: change badges in readme 2022-10-12 17:08:40 +08:00
Noah Hsu 5ea8d62aa4 fix(onedrive): unable to operate if path contains % (close #1965) 2022-10-11 14:21:58 +08:00
Noah Hsu baebc2fbe9 fix: can't delete disabled storage (close #1942) 2022-10-09 22:20:48 +08:00
Noah Hsu 8c69260972 fix(webdav): set mime by ext if it's empty 2022-10-09 19:29:55 +08:00
Noah Hsu 30f992c6a8 feat(onedrive): customize chunk size (close #1927) 2022-10-08 22:23:33 +08:00
Noah Hsu dcaaae366b feat: add support for mega.nz (close 1553) 2022-10-08 22:16:41 +08:00
Jake Liu 284035823f feat: add Google Photo support (#1853)
* feat: add Google Photo support

* fix: fetch all pages

* chore(google_photo): add meta info

Co-authored-by: Noah Hsu <i@nn.ci>
2022-10-07 20:36:56 +08:00
Noah Hsu be8ff92414 docs: replace qq with discord [skip ci] 2022-10-05 14:17:00 +08:00
Noah Hsu a4c846a424 chore(onedrive): set default value for region 2022-10-01 20:09:57 +08:00
Noah Hsu 451e418b18 perf: return cache before check obj to reduce recursion 2022-09-28 21:19:36 +08:00
hdm9527 4e13b1a83c perf: modify onedrive upload chunk size (#1831 close #1790)
improve onedrive upload speed
2022-09-27 20:29:54 +08:00
Noah Hsu 9d2e9887af docs: create FUNDING.yml [skip ci] 2022-09-27 14:41:43 +08:00
Noah Hsu dc73c2e97d fix: custom token expires in doesn't work 2022-09-27 14:23:56 +08:00
Noah Hsu a624121095 ci: manual trigger github actions 2022-09-27 14:12:36 +08:00
Noah Hsu 9d9c79179b feat: custom token expires in 2022-09-27 14:05:00 +08:00
Noah Hsu b7479651e1 fix: incorrect base_path from site_url (close #1830) 2022-09-27 13:56:32 +08:00
Noah Hsu 2fc0ccbfe0 fix: don't init aria2 in new goroutine (close #1752) 2022-09-26 15:11:08 +08:00
Noah Hsu f86ad1dce4 fix: create temp dir perm with 777 (close #1813) 2022-09-26 14:48:59 +08:00
44 changed files with 1041 additions and 158 deletions
+13
View File
@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://alist.nn.ci/guide/sponsor.html']
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.18]
go-version: [1.19]
name: Build
runs-on: ${{ matrix.platform }}
steps:
@@ -3,6 +3,7 @@ name: Close inactive
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-inactive:
@@ -3,6 +3,7 @@ name: Close need info
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-need-info:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.18]
go-version: [1.19]
name: Release
runs-on: ${{ matrix.platform }}
steps:
+1 -1
View File
@@ -7,7 +7,7 @@
Prerequisites:
- [git](https://nodejs.org/zh-cn/)
- [Go 1.18+](https://golang.org/doc/install)
- [Go 1.19+](https://golang.org/doc/install)
- [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/)
+24 -9
View File
@@ -1,30 +1,40 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂️A file list program that supports multiple storage, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
@@ -52,6 +62,11 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@@ -100,4 +115,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
+24 -9
View File
@@ -1,30 +1,40 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
@@ -52,6 +62,11 @@
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
- [x] [阿里云盘分享](https://www.aliyundrive.com/)
- [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览(PDF、markdown、代码、纯文本……)
- [x] 画廊模式下的图像预览
@@ -100,4 +115,4 @@
---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@QQ群](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)
+2 -1
View File
@@ -12,7 +12,8 @@ else
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi
echo "build version: $gitTag"
echo "backend version: $version"
echo "frontend version: $webVersion"
ldflags="\
-w -s \
+2 -2
View File
@@ -61,7 +61,7 @@ the address is defined in config file`,
<-quit
utils.Log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
utils.Log.Fatal("Server Shutdown:", err)
@@ -69,7 +69,7 @@ the address is defined in config file`,
// catching ctx.Done(). timeout of 3 seconds.
select {
case <-ctx.Done():
utils.Log.Println("timeout of 3 seconds.")
utils.Log.Println("timeout of 1 seconds.")
}
utils.Log.Println("Server exiting")
},
+3 -1
View File
@@ -110,7 +110,9 @@ func ParseHttpHeader(str string) map[string]string {
header := make(map[string]string)
for _, value := range strings.Split(str, "&") {
i := strings.Index(value, "=")
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
if i > 0 {
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
}
}
return header
}
+1
View File
@@ -15,6 +15,7 @@ type Addition struct {
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
FamilyID string `json:"family_id"`
RapidUpload bool `json:"rapid_upload"`
NonuseOrc bool `json:"nonuse_orc"`
}
var config = driver.Config{
+35 -38
View File
@@ -186,20 +186,19 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
func (y *Yun189PC) login() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil {
if y.loginParam == nil || !y.NonuseOrc {
if err = y.initLoginParam(); err != nil {
// 验证码也通过错误返回
return err
}
}
defer func() {
// 销毁验证码
y.VCode = ""
// 销毁登陆参数
y.loginParam = nil
// 遇到错误,重新加载登陆参数
if err != nil {
if err != nil && y.NonuseOrc {
if err1 := y.initLoginParam(); err1 != nil {
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
}
@@ -303,44 +302,34 @@ func (y *Yun189PC) initLoginParam() error {
param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
// 判断是否需要验证码
res, err = y.client.R().
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": ACCOUNT_TYPE,
"userName": param.RsaUsername,
}).
Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
if err != nil {
return err
}
y.loginParam = &param
if res.String() != "0" {
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
"token": param.CaptchaToken,
"REQID": param.ReqId,
"rnd": fmt.Sprint(timestamp()),
}).
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
if err != nil {
return fmt.Errorf("failed to obtain verification code")
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
"token": param.CaptchaToken,
"REQID": param.ReqId,
"rnd": fmt.Sprint(timestamp()),
}).
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
if err != nil {
return fmt.Errorf("failed to obtain verification code")
}
if imgRes.Size() > 20 {
if setting.GetStr(conf.OcrApi) != "" && !y.NonuseOrc {
vRes, err := base.RestyClient.R().
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(setting.GetStr(conf.OcrApi))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
return nil
}
}
// 尝试使用ocr
vRes, err := base.RestyClient.R().
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(setting.GetStr(conf.OcrApi))
if err == nil && jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
}
// ocr无法处理,返回验证码图片给前端
if len(y.VCode) != 4 {
return fmt.Errorf("need validate code: data:image/png;base64,%s", base64.StdEncoding.EncodeToString(res.Body()))
}
// 返回验证码图片给前端
return fmt.Errorf(`need img validate code: <img src="data:image/png;base64,%s"/>`, base64.StdEncoding.EncodeToString(imgRes.Body()))
}
return nil
}
@@ -396,6 +385,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
const DEFAULT int64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
requestID := uuid.NewString()
params := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(file.GetName()),
@@ -417,6 +407,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
var initMultiUpload InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &initMultiUpload)
if err != nil {
return err
@@ -451,6 +442,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64),
"uploadFileId": initMultiUpload.Data.UploadFileID,
@@ -486,6 +478,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": initMultiUpload.Data.UploadFileID,
"fileMd5": fileMd5Hex,
@@ -542,6 +535,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
requestID := uuid.NewString()
// 检测是否支持快传
params := Params{
"parentFolderId": dstDir.GetID(),
@@ -564,6 +558,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &uploadInfo)
if err != nil {
return err
@@ -575,6 +570,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
@@ -611,6 +607,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",
+2
View File
@@ -11,9 +11,11 @@ import (
_ "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/google_photo"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/quark"
+1 -1
View File
@@ -33,7 +33,7 @@ func fileToObj(f File) *model.ObjThumb {
ID: f.Id,
Name: f.Name,
Size: size,
Modified: time.Time{},
Modified: f.ModifiedTime,
IsFolder: f.MimeType == "application/vnd.google-apps.folder",
},
Thumbnail: model.Thumbnail{},
+170
View File
@@ -0,0 +1,170 @@
package google_photo
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"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/go-resty/resty/v2"
)
type GooglePhoto struct {
model.Storage
Addition
AccessToken string
}
func (d *GooglePhoto) Config() driver.Config {
return config
}
func (d *GooglePhoto) GetAddition() driver.Additional {
return d.Addition
}
func (d *GooglePhoto) 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 *GooglePhoto) Drop(ctx context.Context) error {
return nil
}
func (d *GooglePhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src MediaItem) (model.Obj, error) {
return fileToObj(src), nil
})
}
//func (d *GooglePhoto) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
f, err := d.getMedia(file.GetID())
if err != nil {
return nil, err
}
if strings.Contains(f.MimeType, "image/") {
return &model.Link{
URL: f.BaseURL + "=d",
}, nil
} else if strings.Contains(f.MimeType, "video/") {
return &model.Link{
URL: f.BaseURL + "=dv",
}, nil
}
return &model.Link{}, nil
}
func (d *GooglePhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotSupport
}
func (d *GooglePhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotSupport
}
func (d *GooglePhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var e Error
// Create resumable upload url
postHeaders := map[string]string{
"Authorization": "Bearer " + d.AccessToken,
"Content-type": "application/octet-stream",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Content-Type": stream.GetMimetype(),
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Raw-Size": strconv.FormatInt(stream.GetSize(), 10),
}
url := "https://photoslibrary.googleapis.com/v1/uploads"
res, err := base.NoRedirectClient.R().SetHeaders(postHeaders).
SetError(&e).
Post(url)
if err != nil {
return err
}
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = d.refreshToken()
if err != nil {
return err
}
return d.Put(ctx, dstDir, stream, up)
}
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
//Upload to the Google Photo
postUrl := res.Header().Get("X-Goog-Upload-URL")
//chunkSize := res.Header().Get("X-Goog-Upload-Chunk-Granularity")
postHeaders = map[string]string{
"X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": "0",
}
resp, err := d.request(postUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(stream.GetReadCloser())
}, nil, postHeaders)
if err != nil {
return err
}
//Create MediaItem
createItemUrl := "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"
postHeaders = map[string]string{
"X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": "0",
}
data := base.Json{
"newMediaItems": []base.Json{
{
"description": "item-description",
"simpleMediaItem": base.Json{
"fileName": stream.GetName(),
"uploadToken": string(resp),
},
},
},
}
_, err = d.request(createItemUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil, postHeaders)
return err
}
var _ driver.Driver = (*GooglePhoto)(nil)
+30
View File
@@ -0,0 +1,30 @@
package google_photo
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"`
ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"`
ShowArchive bool `json:"show_archive"`
}
var config = driver.Config{
Name: "GooglePhoto",
OnlyProxy: true,
DefaultRoot: "root",
NoUpload: true,
LocalSort: true,
}
func New() driver.Driver {
return &GooglePhoto{}
}
func init() {
op.RegisterDriver(config, New)
}
+85
View File
@@ -0,0 +1,85 @@
package google_photo
import (
"reflect"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type TokenError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
type Items struct {
NextPageToken string `json:"nextPageToken"`
MediaItems []MediaItem `json:"mediaItems,omitempty"`
Albums []MediaItem `json:"albums,omitempty"`
SharedAlbums []MediaItem `json:"sharedAlbums,omitempty"`
}
type MediaItem struct {
Id string `json:"id"`
Title string `json:"title,omitempty"`
BaseURL string `json:"baseUrl,omitempty"`
CoverPhotoBaseUrl string `json:"coverPhotoBaseUrl,omitempty"`
MimeType string `json:"mimeType,omitempty"`
FileName string `json:"filename,omitempty"`
MediaMetadata MediaMetadata `json:"mediaMetadata,omitempty"`
}
type MediaMetadata struct {
CreationTime time.Time `json:"creationTime"`
Width string `json:"width"`
Height string `json:"height"`
Photo Photo `json:"photo,omitempty"`
Video Video `json:"video,omitempty"`
}
type Photo struct {
}
type Video struct {
}
func fileToObj(f MediaItem) *model.ObjThumb {
if !reflect.DeepEqual(f.MediaMetadata, MediaMetadata{}){
return &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.FileName,
Size: 0,
Modified: f.MediaMetadata.CreationTime,
IsFolder: false,
},
Thumbnail: model.Thumbnail{
Thumbnail: f.BaseURL + "=w100-h100-c",
},
}
}
return &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.Title,
Size: 0,
Modified: time.Time{},
IsFolder: true,
},
Thumbnail: model.Thumbnail{},
}
}
type Error struct {
Error struct {
Errors []struct {
Domain string `json:"domain"`
Reason string `json:"reason"`
Message string `json:"message"`
LocationType string `json:"location_type"`
Location string `json:"location"`
}
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
+186
View File
@@ -0,0 +1,186 @@
package google_photo
import (
"fmt"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
// do others that not defined in Driver interface
const (
FETCH_ALL = "all"
FETCH_ALBUMS = "albums"
FETCH_ROOT = "root"
FETCH_SHARE_ALBUMS = "share_albums"
)
func (d *GooglePhoto) refreshToken() error {
url := "https://www.googleapis.com/oauth2/v4/token"
var resp base.TokenResp
var e TokenError
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).
SetFormData(map[string]string{
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
"refresh_token": d.RefreshToken,
"grant_type": "refresh_token",
}).Post(url)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf(e.Error)
}
d.AccessToken = resp.AccessToken
return nil
}
func (d *GooglePhoto) request(url string, method string, callback base.ReqCallback, resp interface{}, headers map[string]string) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
req.SetHeader("Accept-Encoding", "gzip")
if headers != nil {
req.SetHeaders(headers)
}
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Error
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp, headers)
}
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
return res.Body(), nil
}
func (d *GooglePhoto) getFiles(id string) ([]MediaItem, error) {
switch id {
case FETCH_ALL:
return d.getAllMedias()
case FETCH_ALBUMS:
return d.getAlbums()
case FETCH_SHARE_ALBUMS:
return d.getShareAlbums()
case FETCH_ROOT:
return d.getFakeRoot()
default:
return d.getMedias(id)
}
}
func (d *GooglePhoto) getFakeRoot() ([]MediaItem, error) {
return []MediaItem{
{
Id: FETCH_ALL,
Title: "全部媒体",
},
{
Id: FETCH_ALBUMS,
Title: "全部影集",
},
{
Id: FETCH_SHARE_ALBUMS,
Title: "共享影集",
},
}, nil
}
func (d *GooglePhoto) getAlbums() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/albums",
map[string]string{
"fields": "albums(id,title,coverPhotoBaseUrl),nextPageToken",
"pageSize": "50",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getShareAlbums() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/sharedAlbums",
map[string]string{
"fields": "sharedAlbums(id,title,coverPhotoBaseUrl),nextPageToken",
"pageSize": "50",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getMedias(albumId string) ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/mediaItems:search",
map[string]string{
"fields": "mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",
"pageSize": "100",
"albumId": albumId,
"pageToken": "first",
}, http.MethodPost)
}
func (d *GooglePhoto) getAllMedias() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/mediaItems",
map[string]string{
"fields": "mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",
"pageSize": "100",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getMedia(id string) (MediaItem, error) {
var resp MediaItem
query := map[string]string{
"fields": "baseUrl,mimeType",
}
_, err := d.request(fmt.Sprintf("https://photoslibrary.googleapis.com/v1/mediaItems/%s", id), http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return resp, err
}
return resp, nil
}
func (d *GooglePhoto) fetchItems(url string, query map[string]string, method string) ([]MediaItem, error){
res := make([]MediaItem, 0)
for query["pageToken"] != "" {
if query["pageToken"] == "first" {
query["pageToken"] = ""
}
var resp Items
_, err := d.request(url, method, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return nil, err
}
query["pageToken"] = resp.NextPageToken
res = append(res, resp.MediaItems...)
res = append(res, resp.Albums...)
res = append(res, resp.SharedAlbums...)
}
return res, nil
}
+192
View File
@@ -0,0 +1,192 @@
package mega
import (
"context"
"errors"
"fmt"
"io"
"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/chanio"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/t3rm1n4l/go-mega"
)
type Mega struct {
model.Storage
Addition
c *mega.Mega
}
func (d *Mega) Config() driver.Config {
return config
}
func (d *Mega) GetAddition() driver.Additional {
return d.Addition
}
func (d *Mega) 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
}
d.c = mega.New()
return d.c.Login(d.Email, d.Password)
}
func (d *Mega) Drop(ctx context.Context) error {
return nil
}
func (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if node, ok := dir.(*MegaNode); ok {
nodes, err := d.c.FS.GetChildren(node.Node)
if err != nil {
return nil, err
}
res := make([]model.Obj, 0)
for i := range nodes {
n := nodes[i]
if n.GetType() == mega.FILE || n.GetType() == mega.FOLDER {
res = append(res, &MegaNode{n})
}
}
return res, nil
}
log.Errorf("can't convert: %+v", dir)
return nil, fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Get(ctx context.Context, path string) (model.Obj, error) {
if path == "/" {
n := d.c.FS.GetRoot()
log.Debugf("mega root: %+v", *n)
return &MegaNode{n}, nil
}
return nil, errs.NotSupport
}
func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if node, ok := file.(*MegaNode); ok {
//link, err := d.c.Link(node.Node, true)
//if err != nil {
// return nil, err
//}
//return &model.Link{URL: link}, nil
down, err := d.c.NewDownload(node.Node)
if err != nil {
return nil, err
}
//u := down.GetResourceUrl()
//u = strings.Replace(u, "http", "https", 1)
//return &model.Link{URL: u}, nil
c := chanio.New()
go func() {
defer func() {
_ = recover()
}()
log.Debugf("chunk size: %d", down.Chunks())
for id := 0; id < down.Chunks(); id++ {
chunk, err := down.DownloadChunk(id)
if err != nil {
log.Errorf("mega down: %+v", err)
return
}
log.Debugf("id: %d,len: %d", id, len(chunk))
//_, _, err = down.ChunkLocation(id)
//if err != nil {
// log.Errorf("mega down: %+v", err)
// return
//}
//_, err = c.Write(chunk)
_, err = c.Write(chunk)
}
err := c.Close()
if err != nil {
log.Errorf("mega down: %+v", err)
}
}()
return &model.Link{Data: c}, nil
}
return nil, fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if parentNode, ok := parentDir.(*MegaNode); ok {
_, err := d.c.CreateDir(dirName, parentNode.Node)
return err
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if srcNode, ok := srcObj.(*MegaNode); ok {
if dstNode, ok := dstDir.(*MegaNode); ok {
return d.c.Move(srcNode.Node, dstNode.Node)
}
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if srcNode, ok := srcObj.(*MegaNode); ok {
return d.c.Rename(srcNode.Node, newName)
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *Mega) Remove(ctx context.Context, obj model.Obj) error {
if node, ok := obj.(*MegaNode); ok {
return d.c.Delete(node.Node, false)
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if dstNode, ok := dstDir.(*MegaNode); ok {
u, err := d.c.NewUpload(dstNode.Node, stream.GetName(), stream.GetSize())
if err != nil {
return err
}
for id := 0; id < u.Chunks(); id++ {
_, chkSize, err := u.ChunkLocation(id)
if err != nil {
return err
}
chunk := make([]byte, chkSize)
n, err := io.ReadFull(stream, chunk)
if err != nil && err != io.EOF {
return err
}
if n != len(chunk) {
return errors.New("chunk too short")
}
err = u.UploadChunk(id, chunk)
if err != nil {
return err
}
up(id * 100 / u.Chunks())
}
_, err = u.Finish()
return err
}
return fmt.Errorf("unable to convert dir to mega node")
}
//func (d *Mega) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Mega)(nil)

Some files were not shown because too many files have changed in this diff Show More