Compare commits

...

41 Commits

Author SHA1 Message Date
4cbbda8832 fix(baidu): custom upload part size (close #5757) 2024-05-02 22:30:00 +08:00
Mmx
7bf5014417 ci: cache musl library in docker build workflow (#6392)
* ci: add musl libs into action cache

* build: update Dockerfile.ci
2024-05-02 22:28:13 +08:00
b704bba444 fix(115): disable NoOverwriteUpload (#6409 close #6251)
closed #6251
2024-05-02 22:27:55 +08:00
eecea3febd fix(onedrive): fix Ctime/Mtime (#6397) 2024-05-02 22:27:31 +08:00
0e246a7b0c chore: replace link of vidhub [skip ci] 2024-04-30 14:22:26 +08:00
Mmx
b95df1d745 perf: use io copy with buffer pool (#6389)
* feat: add io methods with buffer

* chore: move io.Copy calls to utils.CopyWithBuffer
2024-04-25 20:11:15 +08:00
ec08ecdf6c fix(baidu_netdisk): cached Ctime/Mtime (#6373 close #6370)
(cherry picked from commit 23542541e4f343d484de1f83ee5c928d2ab6753c)
2024-04-25 20:08:20 +08:00
479fc6d466 fix(webdav): make sure Mtime after Ctime (#6372 close #6371)
* fix(server/webdav) make sure Mtime >= Ctime

* fix(server/webdav) avoid variable 'stream' collides with imported package name
2024-04-24 17:13:30 +08:00
32ddab9b01 feat(123_share): add access token (#6357) 2024-04-24 14:54:01 +08:00
0c9dcec9cd fix: init storages in order (#6346) 2024-04-19 17:22:16 +08:00
793a4ea6ca fix(cloudreve): add domain to the download url if not exists (#6339 close #6265)
* fix: correct the download url got by Cloudreve driver

* fix: add an condition to the correction
2024-04-12 21:45:16 +08:00
c3c5181847 feat(Seafile): add token login (#6324 close #5302) 2024-04-10 21:50:30 +08:00
Mix
cd5a8a011d fix: typo about env of Meilisearch (#6316) 2024-04-08 18:35:23 +08:00
1756036a21 fix(authn): subfolder api is considered as a wrong origin(closes #6294 in #6301) 2024-04-03 14:33:19 +08:00
58c3cb3cf6 fix(s3): don't bind s3 port if s3 is not enabled (#6291) 2024-04-03 10:09:48 +08:00
d8e190406a feat(189pc): add family transfer upload (#6288)
* feat(189pc): add family transfer upload

* fix(189):family transfer file delete
2024-04-02 16:51:02 +08:00
2880ed70ce fix: some typos (#6283)
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-04-02 16:50:30 +08:00
0e86036874 fix(doge): reget client after refresh session (#6277) 2024-03-29 14:56:49 +08:00
e37465e67e feat(crypt): force stream upload for supported drivers (#6270) 2024-03-29 14:42:01 +08:00
d517adde71 docs: use width instead of height for image in Readme (#6282)
* Update README.md

* Update README_cn.md

* Update README_ja.md
2024-03-29 14:40:43 +08:00
8a18f47e68 fix(doge): the temporary access key is only valid for two hours (#6273)
* feat: add doge driver

* doc: 补充readme文档

* fix: 对齐meta信息

* fix: 调整结构体名字,与driver保持一致

* perf: merge to s3

* Rename goge.go to doge.go

* fix: 解决多吉云临时秘钥两个小时过期的问题

* fix: 定时任务在Drop中Stop

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-27 14:22:26 +08:00
cf08aa3668 feat: add doge driver (#6201)
* feat: add doge driver

* doc: 补充readme文档

* fix: 对齐meta信息

* fix: 调整结构体名字,与driver保持一致

* perf: merge to s3

* Rename goge.go to doge.go

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-25 22:53:44 +08:00
9c84b6596f feat: stand-alone port s3 server (#6242)
* feat: single port s3 server

* fix: unable to PUT files if not in root dir
2024-03-24 15:16:00 +08:00
022e0ca292 fix(139): incorrect refreshTokenResp serialization (#6248) 2024-03-24 11:04:55 +08:00
88947f6676 fix(ipfs): url escape filename (#6245 close #6027)
This resolves #6027
2024-03-24 11:03:18 +08:00
Mmx
b07ddfbc13 fix(ci): replace dockerfile tag step may have no effect (#6206) 2024-03-13 15:11:21 +08:00
9a0a63d34c fix(ilanzou): add referer to request header (close #6171) 2024-03-11 20:30:22 +08:00
195c869272 feat(139): refresh token periodically (#6146)
* 139定时刷新token

* fix build fail
2024-03-11 20:10:26 +08:00
bdfc1591bd fix: webauthn logspam (#6181) 2024-03-10 16:48:25 +08:00
82222840fe fix(deps): update golang.org/x/exp digest to 814bf88 (#6144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-09 14:58:36 +08:00
45e009a22c fix(mopan): upload error (close #6158 in #6166) 2024-03-09 14:54:49 +08:00
ac68079a76 feat(seafile): improve features, support access to encrypted library, etc (#6160) 2024-03-08 15:33:42 +08:00
2a17d0c2cd fix: settings reset to default after restart if set to empty (close #6143) 2024-03-05 16:29:26 +08:00
6f6a8e6dfc fix(deps): update github.com/t3rm1n4l/go-mega digest to d494b6a (#6081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 15:12:22 +08:00
7d9ecba99c fix: add m3u8 to default video types (close #6142) 2024-03-04 14:26:00 +08:00
ae6984714d fix: remove default polyfill (#6130 close #6100)
* refactor(setting): replace `polyfill.io``

* fix: remove default polyfill

---------

Co-authored-by: Andy Hsu <i@nn.ci>
2024-03-02 15:36:28 +08:00
d0f88bd1cb feat: s3 server support (#6088 close #5186)
Currently tested: List, Get, Remove
2024-03-02 15:35:10 +08:00
f8b1f87a5f fix: support for Microsoft WebDAV (#6133 close #6104)
* Add support for Microsoft WebDAV

* add import
2024-03-02 14:59:55 +08:00
71e4e1ab6e fix(chaoxing): json cannot unmarshal content.uploadDate (close #6119 in #6124) 2024-03-01 13:37:09 +08:00
7e6522c81e ci: build ffmpeg image with dev version 2024-02-24 18:10:45 +08:00
94a80bccfe fix(feiji): unable to get link (close #6082) 2024-02-24 18:04:08 +08:00
89 changed files with 2214 additions and 356 deletions

44
.air.toml Normal file
View File

@ -0,0 +1,44 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = ["server"]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@ -32,10 +32,21 @@ jobs:
flavor: |
suffix=-ffmpeg,onlatest=true
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary
run: bash build.sh dev docker-multiplatform
@ -63,10 +74,15 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
- name: Replace dockerfile tag
run: |
sed -i -e "s/latest/main/g" Dockerfile.ffmpeg
- name: Build and push with ffmpeg
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.ffmpeg
push: ${{ github.event_name == 'push' }}
tags: ${{ steps.meta-ffmpeg.outputs.tags }}

View File

@ -13,10 +13,21 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: 'stable'
- name: Cache Musl
id: cache-musl
uses: actions/cache@v4
with:
path: build/musl-libs
key: docker-musl-libs
- name: Download Musl Library
if: steps.cache-musl.outputs.cache-hit != 'true'
run: bash build.sh prepare docker-multiplatform
- name: Build go binary
run: bash build.sh release docker-multiplatform
@ -62,6 +73,7 @@ jobs:
id: docker_build_ffmpeg
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.ffmpeg
push: true
tags: ${{ steps.meta-ffmpeg.outputs.tags }}

1
.gitignore vendored
View File

@ -24,6 +24,7 @@ output/
*.json
/build
/data/
/tmp/
/log/
/lang/
/daemon/

View File

@ -3,7 +3,7 @@ ARG TARGETPLATFORM
LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/
WORKDIR /opt/alist/
COPY /${TARGETPLATFORM}/alist ./
COPY /build/${TARGETPLATFORM}/alist ./
COPY entrypoint.sh /entrypoint.sh
RUN apk update && \
apk upgrade --no-cache && \

View File

@ -1,5 +1,5 @@
<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>
<a href="https://alist.nn.ci"><img width="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 storages, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
@ -76,6 +76,7 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -114,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎

View File

@ -1,5 +1,5 @@
<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>
<a href="https://alist.nn.ci"><img width="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">
@ -75,6 +75,7 @@
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [飞机盘](https://www.feijipan.com/)
- [x] [多吉云](https://www.dogecloud.com/product/oss)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -112,7 +113,7 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
### 特别赞助
- [VidHub](https://zh.okaapps.com/product/1659622164?ref=alist) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器iPhoneiPadMacApple TV全平台支持。
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎

View File

@ -1,5 +1,5 @@
<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>
<a href="https://alist.nn.ci"><img width="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">
@ -76,6 +76,7 @@
- [X] Cloudreve
- [x] [Dropbox](https://www.dropbox.com/)
- [x] [FeijiPan](https://www.feijipan.com/)
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
- [x] デプロイが簡単で、すぐに使える
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
- [x] ギャラリーモードでの画像プレビュー
@ -114,7 +115,7 @@ https://alist.nn.ci/guide/sponsor.html
### スペシャルスポンサー
- [VidHub](https://okaapps.com/product/1659622164?ref=alist) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - An elegant cloud video player within the Apple ecosystem. Support for iPhone, iPad, Mac, and Apple TV.
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎

View File

@ -96,17 +96,24 @@ BuildDocker() {
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
}
BuildDockerMultiplatform() {
PrepareBuildDocker
PrepareBuildDockerMusl() {
mkdir -p build/musl-libs
BASE="https://musl.cc/"
FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross)
for i in "${FILES[@]}"; do
url="${BASE}${i}.tgz"
curl -L -o "${i}.tgz" "${url}"
sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
rm -f "${i}.tgz"
lib_tgz="build/${i}.tgz"
curl -L -o "${lib_tgz}" "${url}"
tar xf "${lib_tgz}" --strip-components 1 -C build/musl-libs
rm -f "${lib_tgz}"
done
}
BuildDockerMultiplatform() {
PrepareBuildDocker
# run PrepareBuildDockerMusl before build
export PATH=$PATH:$PWD/build/musl-libs/bin
docker_lflags="--extldflags '-static -fpic' $ldflags"
export CGO_ENABLED=1
@ -122,7 +129,7 @@ BuildDockerMultiplatform() {
export GOARCH=$arch
export CC=${cgo_cc}
echo "building for $os_arch"
go build -o ./$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
@ -136,7 +143,7 @@ BuildDockerMultiplatform() {
export GOARM=${GO_ARM[$i]}
export CC=${cgo_cc}
echo "building for $docker_arch"
go build -o ./${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
done
}
@ -289,6 +296,10 @@ elif [ "$1" = "release" ]; then
BuildRelease
MakeRelease "md5.txt"
fi
elif [ "$1" = "prepare" ]; then
if [ "$2" = "docker-multiplatform" ]; then
PrepareBuildDockerMusl
fi
else
echo -e "Parameter error"
fi

View File

@ -91,6 +91,27 @@ the address is defined in config file`,
}
}()
}
if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
s3r := gin.New()
s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
server.InitS3(s3r)
s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
utils.Log.Infof("start S3 server @ %s", s3Base)
go func() {
var err error
if conf.Conf.S3.SSL {
httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
}
if !conf.Conf.S3.SSL {
httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
err = httpSrv.ListenAndServe()
}
if err != nil && !errors.Is(err, http.ErrServerClosed) {
utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
}
}()
}
// Wait for interrupt signal to gracefully shutdown the server with
// a timeout of 1 second.
quit := make(chan os.Signal, 1)

View File

@ -19,7 +19,7 @@ var config = driver.Config{
DefaultRoot: "0",
//OnlyProxy: true,
//OnlyLocal: true,
NoOverwriteUpload: true,
//NoOverwriteUpload: true,
}
func init() {

View File

@ -194,7 +194,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return err
}
_, err = tempFile.Seek(0, io.SeekStart)

View File

@ -4,8 +4,11 @@ import (
"context"
"encoding/base64"
"fmt"
"golang.org/x/time/rate"
"net/http"
"net/url"
"sync"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -19,6 +22,7 @@ import (
type Pan123Share struct {
model.Storage
Addition
apiRateLimit sync.Map
}
func (d *Pan123Share) Config() driver.Config {
@ -146,4 +150,11 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
// return nil, errs.NotSupport
//}
func (d *Pan123Share) APIRateLimit(api string) bool {
limiter, _ := d.apiRateLimit.LoadOrStore(api,
rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
ins := limiter.(*rate.Limiter)
return ins.Allow()
}
var _ driver.Driver = (*Pan123Share)(nil)

View File

@ -7,10 +7,11 @@ import (
type Addition struct {
ShareKey string `json:"sharekey" required:"true"`
SharePwd string `json:"sharepassword" required:"true"`
SharePwd string `json:"sharepassword"`
driver.RootID
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"`
AccessToken string `json:"accesstoken" type:"text"`
}
var config = driver.Config{

View File

@ -2,8 +2,15 @@ package _123Share
import (
"errors"
"fmt"
"hash/crc32"
"math"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
@ -15,20 +22,45 @@ const (
Api = "https://www.123pan.com/api"
AApi = "https://www.123pan.com/a/api"
BApi = "https://www.123pan.com/b/api"
MainApi = Api
MainApi = BApi
FileList = MainApi + "/share/get"
DownloadInfo = MainApi + "/share/download/info"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
)
func signPath(path string, os string, version string) (k string, v string) {
table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
now := time.Now().In(time.FixedZone("CST", 8*3600))
timestamp := fmt.Sprint(now.Unix())
nowStr := []byte(now.Format("200601021504"))
for i := 0; i < len(nowStr); i++ {
nowStr[i] = table[nowStr[i]-48]
}
timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
}
func GetApi(rawUrl string) string {
u, _ := url.Parse(rawUrl)
query := u.Query()
query.Add(signPath(u.Path, "web", "3"))
u.RawQuery = query.Encode()
return u.String()
}
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"user-agent": "Dart/2.19(dart:io)",
"platform": "android",
"app-version": "36",
"origin": "https://www.123pan.com",
"referer": "https://www.123pan.com/",
"authorization": "Bearer " + d.AccessToken,
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
"platform": "web",
"app-version": "3",
//"user-agent": base.UserAgent,
})
if callback != nil {
callback(req)
@ -36,7 +68,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
res, err := req.Execute(method, GetApi(url))
if err != nil {
return nil, err
}
@ -52,6 +84,10 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
page := 1
res := make([]File, 0)
for {
if !d.APIRateLimit(FileList) {
time.Sleep(time.Millisecond * 200)
continue
}
var resp Files
query := map[string]string{
"limit": "100",

View File

@ -8,18 +8,21 @@ import (
"net/http"
"strconv"
"strings"
"time"
"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/pkg/cron"
log "github.com/sirupsen/logrus"
)
type Yun139 struct {
model.Storage
Addition
cron *cron.Cron
Account string
}
@ -35,6 +38,13 @@ func (d *Yun139) Init(ctx context.Context) error {
if d.Authorization == "" {
return fmt.Errorf("authorization is empty")
}
d.cron = cron.NewCron(time.Hour * 24 * 7)
d.cron.Do(func() {
err := d.refreshToken()
if err != nil {
log.Errorf("%+v", err)
}
})
switch d.Addition.Type {
case MetaPersonalNew:
if len(d.Addition.RootFolderID) == 0 {
@ -72,6 +82,9 @@ func (d *Yun139) Init(ctx context.Context) error {
}
func (d *Yun139) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil
}

View File

@ -1,5 +1,9 @@
package _139
import (
"encoding/xml"
)
const (
MetaPersonal string = "personal"
MetaFamily string = "family"
@ -230,3 +234,12 @@ type PersonalUploadResp struct {
UploadId string `json:"uploadId"`
}
}
type RefreshTokenResp struct {
XMLName xml.Name `xml:"root"`
Return string `xml:"return"`
Token string `xml:"token"`
Expiretime int32 `xml:"expiretime"`
AccessToken string `xml:"accessToken"`
Desc string `xml:"desc"`
}

View File

@ -15,6 +15,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/pkg/utils/random"
"github.com/alist-org/alist/v3/internal/op"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
@ -52,6 +53,32 @@ func getTime(t string) time.Time {
return stamp
}
func (d *Yun139) refreshToken() error {
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
var resp RefreshTokenResp
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
if err != nil {
return err
}
decodeStr := string(decode)
splits := strings.Split(decodeStr, ":")
reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
_, err = base.RestyClient.R().
ForceContentType("application/xml").
SetBody(reqBody).
SetResult(&resp).
Post(url)
if err != nil {
return err
}
if resp.Return != "0" {
return fmt.Errorf("failed to refresh token: %s", resp.Desc)
}
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
op.MustSaveDriverStorage(d)
return nil
}
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
url := "https://yun.139.com" + pathname
req := base.RestyClient.R()

View File

@ -1,6 +1,7 @@
package _189pc
import (
"container/ring"
"context"
"net/http"
"strconv"
@ -28,6 +29,9 @@ type Cloud189PC struct {
uploadThread int
familyTransferFolder *ring.Ring
cleanFamilyTransferFile func()
storageConfig driver.Config
}
@ -52,7 +56,6 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
}
if !y.isFamily() && y.RootFolderID == "" {
y.RootFolderID = "-11"
y.FamilyID = ""
}
// 限制上传线程数
@ -79,11 +82,24 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
}
// 处理家庭云ID
if y.isFamily() && y.FamilyID == "" {
if y.FamilyID == "" {
if y.FamilyID, err = y.getFamilyID(); err != nil {
return err
}
}
// 创建中转文件夹,防止重名文件
if y.FamilyTransfer {
if y.familyTransferFolder, err = y.createFamilyTransferFolder(32); err != nil {
return err
}
}
y.cleanFamilyTransferFile = utils.NewThrottle2(time.Minute, func() {
if err := y.cleanFamilyTransfer(context.TODO()); err != nil {
utils.Log.Errorf("cleanFamilyTransferFolderError:%s", err)
}
})
return
}
@ -92,7 +108,7 @@ func (y *Cloud189PC) Drop(ctx context.Context) error {
}
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return y.getFiles(ctx, dir.GetID())
return y.getFiles(ctx, dir.GetID(), y.isFamily())
}
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
@ -100,8 +116,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
URL string `json:"fileDownloadUrl"`
}
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/getFileDownloadUrl.action"
@ -109,7 +126,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParam("fileId", file.GetID())
if y.isFamily() {
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
})
@ -119,7 +136,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
"flag": "1",
})
}
}, &downloadUrl)
}, &downloadUrl, isFamily)
if err != nil {
return nil, err
}
@ -156,8 +173,9 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
}
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
isFamily := y.isFamily()
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/createFolder.action"
@ -169,7 +187,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"folderName": dirName,
"relativePath": "",
})
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentDir.GetID(),
@ -179,7 +197,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
"parentFolderId": parentDir.GetID(),
})
}
}, &newFolder)
}, &newFolder, isFamily)
if err != nil {
return nil, err
}
@ -187,27 +205,14 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
}
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "MOVE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return nil, err
}
@ -218,10 +223,11 @@ func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.
}
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
isFamily := y.isFamily()
queryParam := make(map[string]string)
fullUrl := API_URL
method := http.MethodPost
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
method = http.MethodGet
queryParam["familyId"] = y.FamilyID
@ -245,7 +251,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
_, err := y.request(fullUrl, method, func(req *resty.Request) {
req.SetContext(ctx).SetQueryParams(queryParam)
}, nil, newObj)
}, nil, newObj, isFamily)
if err != nil {
return nil, err
}
@ -253,28 +259,15 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
}
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "COPY",
"taskInfos": MustString(utils.Json.MarshalToString(
[]BatchTaskInfo{
{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
},
})),
"targetFolderId": dstDir.GetID(),
"targetFileName": dstDir.GetName(),
})
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
isFamily := y.isFamily()
other := map[string]string{"targetFileName": dstDir.GetName()}
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
})
if err != nil {
return err
}
@ -282,27 +275,13 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"type": "DELETE",
"taskInfos": MustString(utils.Json.MarshalToString(
[]*BatchTaskInfo{
{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
},
})),
})
isFamily := y.isFamily()
if y.isFamily() {
req.SetFormData(map[string]string{
"familyId": y.FamilyID,
})
}
}, &resp)
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
FileId: obj.GetID(),
FileName: obj.GetName(),
IsFolder: BoolToNumber(obj.IsDir()),
})
if err != nil {
return err
}
@ -310,25 +289,73 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
}
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (newObj model.Obj, err error) {
overwrite := true
isFamily := y.isFamily()
// 响应时间长,按需启用
if y.Addition.RapidUpload {
if newObj, err := y.RapidUpload(ctx, dstDir, stream); err == nil {
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
return newObj, nil
}
}
switch y.UploadMethod {
case "old":
return y.OldUpload(ctx, dstDir, stream, up)
uploadMethod := y.UploadMethod
if stream.IsForceStreamUpload() {
uploadMethod = "stream"
}
// 旧版上传家庭云也有限制
if uploadMethod == "old" {
return y.OldUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
// 开启家庭云转存
if !isFamily && y.FamilyTransfer {
// 修改上传目标为家庭云文件夹
transferDstDir := dstDir
dstDir = (y.familyTransferFolder.Value).(*Cloud189Folder)
y.familyTransferFolder = y.familyTransferFolder.Next()
isFamily = true
overwrite = false
defer func() {
if newObj != nil {
// 批量任务有概率删不掉
y.cleanFamilyTransferFile()
// 转存家庭云文件到个人云
err = y.SaveFamilyFileToPersonCloud(context.TODO(), y.FamilyID, newObj, transferDstDir, true)
task := BatchTaskInfo{
FileId: newObj.GetID(),
FileName: newObj.GetName(),
IsFolder: BoolToNumber(newObj.IsDir()),
}
// 删除源文件
if resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, task); err == nil {
y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
// 永久删除
if resp, err := y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, task); err == nil {
y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
}
}
newObj = nil
}
}()
}
switch uploadMethod {
case "rapid":
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
case "stream":
if stream.GetSize() == 0 {
return y.FastUpload(ctx, dstDir, stream, up)
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
fallthrough
default:
return y.StreamUpload(ctx, dstDir, stream, up)
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
}
}

View File

@ -192,3 +192,19 @@ func partSize(size int64) int64 {
}
return DEFAULT
}
func isBool(bs ...bool) bool {
for _, b := range bs {
if b {
return true
}
}
return false
}
func IF[V any](o bool, t V, f V) V {
if o {
return t
}
return f
}

View File

@ -16,6 +16,7 @@ type Addition struct {
FamilyID string `json:"family_id"`
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
FamilyTransfer bool `json:"family_transfer"`
RapidUpload bool `json:"rapid_upload"`
NoUseOcr bool `json:"no_use_ocr"`
}

View File

@ -3,10 +3,11 @@ package _189pc
import (
"encoding/xml"
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"sort"
"strings"
"time"
"github.com/alist-org/alist/v3/pkg/utils"
)
// 居然有四种返回方式
@ -242,7 +243,12 @@ type BatchTaskInfo struct {
// IsFolder 是否是文件夹0-否1-是
IsFolder int `json:"isFolder"`
// SrcParentId 文件所在父目录ID
//SrcParentId string `json:"srcParentId"`
SrcParentId string `json:"srcParentId,omitempty"`
/* 冲突管理 */
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
DealWay int `json:"dealWay,omitempty"`
IsConflict int `json:"isConflict,omitempty"`
}
/* 上传部分 */
@ -355,6 +361,14 @@ type BatchTaskStateResp struct {
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中4 完成
}
type BatchTaskConflictTaskInfoResp struct {
SessionKey string `json:"sessionKey"`
TargetFolderID int `json:"targetFolderId"`
TaskID string `json:"taskId"`
TaskInfos []BatchTaskInfo
TaskType int `json:"taskType"`
}
/* query 加密参数*/
type Params map[string]string

View File

@ -2,6 +2,7 @@ package _189pc
import (
"bytes"
"container/ring"
"context"
"crypto/md5"
"encoding/base64"
@ -54,11 +55,11 @@ const (
CHANNEL_ID = "web_cloud.189.cn"
)
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
dateOfGmt := getHttpDateStr()
sessionKey := y.tokenInfo.SessionKey
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
if isFamily {
sessionKey = y.tokenInfo.FamilySessionKey
sessionSecret = y.tokenInfo.FamilySessionSecret
}
@ -72,9 +73,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]stri
return header
}
func (y *Cloud189PC) EncryptParams(params Params) string {
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
sessionSecret := y.tokenInfo.SessionSecret
if y.isFamily() {
if isFamily {
sessionSecret = y.tokenInfo.FamilySessionSecret
}
if params != nil {
@ -83,17 +84,17 @@ func (y *Cloud189PC) EncryptParams(params Params) string {
return ""
}
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
req := y.client.R().SetQueryParams(clientSuffix())
// 设置params
paramsData := y.EncryptParams(params)
paramsData := y.EncryptParams(params, isBool(isFamily...))
if paramsData != "" {
req.SetQueryParam("params", paramsData)
}
// Signature
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
var erron RespErr
req.SetError(&erron)
@ -129,15 +130,15 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
return res.Body(), nil
}
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp)
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp)
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
}
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
if err != nil {
return nil, err
@ -154,7 +155,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
if sign {
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
req.Header.Add(key, value)
}
}
@ -181,9 +182,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
}
return body, nil
}
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
fullUrl := API_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family/file"
}
fullUrl += "/listFiles.action"
@ -201,7 +202,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"pageNum": fmt.Sprint(pageNum),
"pageSize": "130",
})
if y.isFamily() {
if isFamily {
r.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"orderBy": toFamilyOrderBy(y.OrderBy),
@ -214,7 +215,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj,
"descending": toDesc(y.OrderDirection),
})
}
}, &resp)
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -437,7 +438,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
// 普通上传
// 无法上传大小为0的文件
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
var sliceSize = partSize(file.GetSize())
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
lastPartSize := file.GetSize() % sliceSize
@ -454,7 +455,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
fullUrl += "/family"
} else {
@ -466,7 +467,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
var initMultiUpload InitMultiUploadResp
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &initMultiUpload)
}, params, &initMultiUpload, isFamily)
if err != nil {
return nil, err
}
@ -502,14 +503,14 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
threadG.Go(func(ctx context.Context) error {
uploadUrls, err := y.GetMultiUploadUrls(ctx, initMultiUpload.Data.UploadFileID, partInfo)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
if err != nil {
return err
}
// step.4 上传切片
uploadUrl := uploadUrls[0]
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily)
if err != nil {
return err
}
@ -538,21 +539,21 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
"sliceMd5": sliceMd5Hex,
"lazyCheck": "1",
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
return resp.toFile(), nil
}
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
fileMd5 := stream.GetHash().GetHash(utils.MD5)
if len(fileMd5) < utils.MD5.Width {
return nil, errors.New("invalid hash")
}
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -561,11 +562,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
return nil, errors.New("rapid upload fail")
}
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId)
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
}
// 快传
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -594,7 +595,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
silceMd5.Reset()
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
return nil, err
}
md5Byte := silceMd5.Sum(nil)
@ -609,7 +610,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
@ -628,13 +629,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
"sliceSize": fmt.Sprint(sliceSize),
"sliceMd5": sliceMd5Hex,
}
if y.isFamily() {
if isFamily {
params.Set("familyId", y.FamilyID)
}
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
}, params, &uploadInfo)
}, params, &uploadInfo, isFamily)
if err != nil {
return nil, err
}
@ -659,7 +660,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
i, uploadPart := i, uploadPart
threadG.Go(func(ctx context.Context) error {
// step.3 获取上传链接
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart)
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
if err != nil {
return err
}
@ -671,7 +672,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}
// step.4 上传切片
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize))
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
if err != nil {
return err
}
@ -698,8 +699,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
}, Params{
"uploadFileId": uploadInfo.UploadFileID,
"isLog": "0",
"opertype": "3",
}, &resp)
"opertype": IF(overwrite, "3", "1"),
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -708,9 +709,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
// 获取上传切片信息
// 对http body有大小限制分片信息太多会出错
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
fullUrl := UPLOAD_URL
if y.isFamily() {
if isFamily {
fullUrl += "/family"
} else {
fullUrl += "/person"
@ -723,7 +724,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}, Params{
"uploadFileId": uploadFileId,
"partInfo": strings.Join(partInfo, ","),
}, &uploadUrlsResp)
}, &uploadUrlsResp, isFamily)
if err != nil {
return nil, err
}
@ -752,7 +753,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string
}
// 旧版本上传,家庭云不支持覆盖
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
tempFile, err := file.CacheFullInTempFile()
if err != nil {
return nil, err
@ -763,7 +764,7 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
}
// 创建上传会话
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()))
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()), isFamily)
if err != nil {
return nil, err
}
@ -780,14 +781,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"Expect": "100-continue",
}
if y.isFamily() {
if isFamily {
header["FamilyId"] = fmt.Sprint(y.FamilyID)
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
} else {
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
}
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
return nil, err
}
@ -802,10 +803,10 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
"uploadFileId": fmt.Sprint(status.UploadFileId),
"resumePolicy": "1",
})
if y.isFamily() {
if isFamily {
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
}
}, &status)
}, &status, isFamily)
if err != nil {
return nil, err
}
@ -815,20 +816,20 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
}
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId)
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
}
// 创建上传会话
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string) (*CreateUploadFileResp, error) {
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
var uploadInfo CreateUploadFileResp
fullUrl := API_URL + "/createUploadFile.action"
if y.isFamily() {
if isFamily {
fullUrl = API_URL + "/family/file/createFamilyFile.action"
}
_, err := y.post(fullUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetQueryParams(map[string]string{
"familyId": y.FamilyID,
"parentId": parentID,
@ -849,7 +850,7 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
"isLog": "0",
})
}
}, &uploadInfo)
}, &uploadInfo, isFamily)
if err != nil {
return nil, err
@ -858,11 +859,11 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
}
// 提交上传文件
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64) (model.Obj, error) {
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
var resp OldCommitUploadFileResp
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
req.SetContext(ctx)
if y.isFamily() {
if isFamily {
req.SetHeaders(map[string]string{
"ResumePolicy": "1",
"UploadFileId": fmt.Sprint(uploadFileID),
@ -870,13 +871,13 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
})
} else {
req.SetFormData(map[string]string{
"opertype": "3",
"opertype": IF(overwrite, "3", "1"),
"resumePolicy": "1",
"uploadFileId": fmt.Sprint(uploadFileID),
"isLog": "0",
})
}
}, &resp)
}, &resp, isFamily)
if err != nil {
return nil, err
}
@ -895,10 +896,100 @@ func (y *Cloud189PC) isLogin() bool {
return err == nil
}
// 创建家庭云中转文件夹
func (y *Cloud189PC) createFamilyTransferFolder(count int) (*ring.Ring, error) {
folders := ring.New(count)
var rootFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"folderName": "FamilyTransferFolder",
"familyId": y.FamilyID,
})
}, &rootFolder, true)
if err != nil {
return nil, err
}
folderCount := 0
// 获取已有目录
files, err := y.getFiles(context.TODO(), rootFolder.GetID(), true)
if err != nil {
return nil, err
}
for _, file := range files {
if folder, ok := file.(*Cloud189Folder); ok {
folders.Value = folder
folders = folders.Next()
folderCount++
}
}
// 创建新的目录
for folderCount < count {
var newFolder Cloud189Folder
_, err := y.post(API_URL+"/family/file/createFolder.action", func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"folderName": uuid.NewString(),
"familyId": y.FamilyID,
"parentId": rootFolder.GetID(),
})
}, &newFolder, true)
if err != nil {
return nil, err
}
folders.Value = &newFolder
folders = folders.Next()
folderCount++
}
return folders, nil
}
// 清理中转文件夹
func (y *Cloud189PC) cleanFamilyTransfer(ctx context.Context) error {
var tasks []BatchTaskInfo
r := y.familyTransferFolder
for p := r.Next(); p != r; p = p.Next() {
folder := p.Value.(*Cloud189Folder)
files, err := y.getFiles(ctx, folder.GetID(), true)
if err != nil {
return err
}
for _, file := range files {
tasks = append(tasks, BatchTaskInfo{
FileId: file.GetID(),
FileName: file.GetName(),
IsFolder: BoolToNumber(file.IsDir()),
})
}
}
if len(tasks) > 0 {
// 删除
resp, err := y.CreateBatchTask("DELETE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("DELETE", resp.TaskID, time.Second)
if err != nil {
return err
}
// 永久删除
resp, err = y.CreateBatchTask("CLEAR_RECYCLE", y.FamilyID, "", nil, tasks...)
if err != nil {
return err
}
err = y.WaitBatchTask("CLEAR_RECYCLE", resp.TaskID, time.Second)
return err
}
return nil
}
// 获取家庭云所有用户信息
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
var resp FamilyInfoListResp
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
if err != nil {
return nil, err
}
@ -922,6 +1013,73 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
return fmt.Sprint(infos[0].FamilyID), nil
}
// 保存家庭云中的文件到个人云
func (y *Cloud189PC) SaveFamilyFileToPersonCloud(ctx context.Context, familyId string, srcObj, dstDir model.Obj, overwrite bool) error {
// _, err := y.post(API_URL+"/family/file/saveFileToMember.action", func(req *resty.Request) {
// req.SetQueryParams(map[string]string{
// "channelId": "home",
// "familyId": familyId,
// "destParentId": destParentId,
// "fileIdList": familyFileId,
// })
// }, nil)
// return err
task := BatchTaskInfo{
FileId: srcObj.GetID(),
FileName: srcObj.GetName(),
IsFolder: BoolToNumber(srcObj.IsDir()),
}
resp, err := y.CreateBatchTask("COPY", familyId, dstDir.GetID(), map[string]string{
"groupId": "null",
"copyType": "2",
"shareId": "null",
}, task)
if err != nil {
return err
}
for {
state, err := y.CheckBatchTask("COPY", resp.TaskID)
if err != nil {
return err
}
switch state.TaskStatus {
case 2:
task.DealWay = IF(overwrite, 3, 2)
// 冲突时覆盖文件
if err := y.ManageBatchTask("COPY", resp.TaskID, dstDir.GetID(), task); err != nil {
return err
}
case 4:
return nil
}
time.Sleep(time.Millisecond * 400)
}
}
func (y *Cloud189PC) CreateBatchTask(aType string, familyID string, targetFolderId string, other map[string]string, taskInfos ...BatchTaskInfo) (*CreateBatchTaskResp, error) {
var resp CreateBatchTaskResp
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
if targetFolderId != "" {
req.SetFormData(map[string]string{"targetFolderId": targetFolderId})
}
if familyID != "" {
req.SetFormData(map[string]string{"familyId": familyID})
}
req.SetFormData(other)
}, &resp, familyID != "")
if err != nil {
return nil, err
}
return &resp, nil
}
// 检测任务状态
func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
var resp BatchTaskStateResp
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
@ -936,6 +1094,37 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
return &resp, nil
}
// 获取冲突的任务信息
func (y *Cloud189PC) GetConflictTaskInfo(aType string, taskID string) (*BatchTaskConflictTaskInfoResp, error) {
var resp BatchTaskConflictTaskInfoResp
_, err := y.post(API_URL+"/batch/getConflictTaskInfo.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"type": aType,
"taskId": taskID,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
// 处理冲突
func (y *Cloud189PC) ManageBatchTask(aType string, taskID string, targetFolderId string, taskInfos ...BatchTaskInfo) error {
_, err := y.post(API_URL+"/batch/manageBatchTask.action", func(req *resty.Request) {
req.SetFormData(map[string]string{
"targetFolderId": targetFolderId,
"type": aType,
"taskId": taskID,
"taskInfos": MustString(utils.Json.MarshalToString(taskInfos)),
})
}, nil)
return err
}
var ErrIsConflict = errors.New("there is a conflict with the target object")
// 等待任务完成
func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
for {
state, err := y.CheckBatchTask(aType, taskID)
@ -944,7 +1133,7 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
}
switch state.TaskStatus {
case 2:
return errors.New("there is a conflict with the target object")
return ErrIsConflict
case 4:
return nil
}

View File

@ -194,7 +194,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
}
if d.RapidUpload {
buf := bytes.NewBuffer(make([]byte, 0, 1024))
io.CopyN(buf, file, 1024)
utils.CopyWithBufferN(buf, file, 1024)
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
if localFile != nil {
if _, err := localFile.Seek(0, io.SeekStart); err != nil {

View File

@ -136,7 +136,7 @@ func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error
if err != nil {
return "", err
}
_, err = io.CopyN(buf, reader, length)
_, err = utils.CopyWithBufferN(buf, reader, length)
if err != nil {
return "", err
}
@ -164,7 +164,7 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
createData["part_info_list"] = makePartInfos(count)
// rapid upload
rapidUpload := stream.GetSize() > 100*utils.KB && d.RapidUpload
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
if rapidUpload {
log.Debugf("[aliyundrive_open] start cal pre_hash")
// read 1024 bytes to calculate pre hash
@ -242,13 +242,16 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
if remain := stream.GetSize() - offset; length > remain {
length = remain
}
//rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
rd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
if rapidUpload {
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
if err != nil {
return nil, err
}
rd = utils.NewMultiReadable(srd)
}
err = retry.Do(func() error {
//rd.Reset()
rd.Reset()
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
},
retry.Attempts(3),

View File

@ -165,9 +165,16 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = stream.CreateTime().Unix()
newFile.Mtime = stream.ModTime().Unix()
return fileToObj(newFile), nil
}
// Put
//
// **注意**: 截至 2024/04/20 百度云盘 api 接口返回的时间永远是当前时间,而不是文件时间。
// 而实际上云盘存储的时间是文件时间,所以此处需要覆盖时间,保证缓存与云盘的数据一致
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
// rapid upload
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
@ -204,7 +211,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}
@ -245,9 +252,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
log.Debugf("%+v", precreateResp)
if precreateResp.ReturnType == 2 {
//rapid upload, since got md5 match from baidu server
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
precreateResp.File.Ctime = ctime
precreateResp.File.Mtime = mtime
return fileToObj(precreateResp.File), nil
}
}
@ -298,6 +305,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
if err != nil {
return nil, err
}
// 修复时间,具体原因见 Put 方法注释的 **注意**
newFile.Ctime = ctime
newFile.Mtime = mtime
return fileToObj(newFile), nil
}

View File

@ -8,15 +8,16 @@ import (
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
driver.RootPath
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
AccessToken string
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
CustomUploadPartSize int64 `json:"custom_upload_part_size" default:"0" help:"0 for auto"`
}
var config = driver.Config{

View File

@ -249,6 +249,9 @@ const (
)
func (d *BaiduNetdisk) getSliceSize() int64 {
if d.CustomUploadPartSize != 0 {
return d.CustomUploadPartSize
}
switch d.vipType {
case 1:
return VipSliceSize

View File

@ -261,7 +261,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
if i == count {
byteSize = lastBlockSize
}
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
if err != nil && err != io.EOF {
return nil, err
}

View File

@ -229,7 +229,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
if err != nil {
return err
}
_, err = io.Copy(filePart, stream)
_, err = utils.CopyWithBuffer(filePart, stream)
if err != nil {
return err
}

View File

@ -139,7 +139,7 @@ type File struct {
Topsort int `json:"topsort"`
Restype string `json:"restype"`
Size int_str `json:"size"`
UploadDate string `json:"uploadDate"`
UploadDate int64 `json:"uploadDate"`
FileSize string `json:"fileSize"`
Name string `json:"name"`
FileID string `json:"fileId"`
@ -265,10 +265,7 @@ func fileToObj(f File) *model.Object {
IsFolder: true,
}
}
paserTime, err := time.Parse("2006-01-02 15:04", f.Content.UploadDate)
if err != nil {
paserTime = time.Now()
}
paserTime := time.UnixMilli(f.Content.UploadDate)
return &model.Object{
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
Name: f.Content.Name,

View File

@ -71,6 +71,9 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
if err != nil {
return nil, err
}
if strings.HasPrefix(dUrl, "/api") {
dUrl = d.Address + dUrl
}
return &model.Link{
URL: dUrl,
}, nil

View File

@ -3,7 +3,6 @@ package crypt
import (
"context"
"fmt"
"github.com/alist-org/alist/v3/internal/stream"
"io"
stdpath "path"
"regexp"
@ -14,6 +13,7 @@ import (
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
@ -160,7 +160,7 @@ func (d *Crypt) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
// discarding hash as it's encrypted
}
if d.Thumbnail && thumb == "" {
thumb = utils.EncodePath(common.GetApiUrl(nil) + stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
thumb = utils.EncodePath(common.GetApiUrl(nil)+stdpath.Join("/d", args.ReqPath, ".thumbnails", name+".webp"), true)
}
if !ok && !d.Thumbnail {
result = append(result, &objRes)
@ -389,10 +389,11 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
Modified: streamer.ModTime(),
IsFolder: streamer.IsDir(),
},
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
Exist: streamer.GetExist(),
Reader: wrappedIn,
Mimetype: "application/octet-stream",
WebPutAsTask: streamer.NeedStore(),
ForceStreamUpload: true,
Exist: streamer.GetExist(),
}
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
if err != nil {

View File

@ -123,14 +123,14 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
query.Set("devType", "6")
query.Set("devCode", d.UUID)
query.Set("devModel", "chrome")
query.Set("devVersion", "120")
query.Set("devVersion", d.conf.devVersion)
query.Set("appVersion", "")
ts, err := getTimestamp(d.conf.secret)
if err != nil {
return nil, err
}
query.Set("timestamp", ts)
//query.Set("appToken", d.Token)
query.Set("appToken", d.Token)
query.Set("enable", "1")
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
if err != nil {
@ -143,7 +143,21 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}
query.Set("auth", hex.EncodeToString(auth))
u.RawQuery = query.Encode()
link := model.Link{URL: u.String()}
realURL := u.String()
// get the url after redirect
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
//"Origin": d.conf.site,
"Referer": d.conf.site + "/",
}).Get(realURL)
if err != nil {
return nil, err
}
if res.StatusCode() == 302 {
realURL = res.Header().Get("location")
} else {
return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
}
link := model.Link{URL: realURL}
return &link, nil
}
@ -257,7 +271,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
defer func() {
_ = tempFile.Close()
}()
if _, err = io.Copy(h, tempFile); err != nil {
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
return nil, err
}
_, err = tempFile.Seek(0, io.SeekStart)

View File

@ -15,11 +15,13 @@ type Addition struct {
}
type Conf struct {
base string
secret []byte
bucket string
unproved string
proved string
base string
secret []byte
bucket string
unproved string
proved string
devVersion string
site string
}
func init() {
@ -39,11 +41,13 @@ func init() {
NoOverwriteUpload: false,
},
conf: Conf{
base: "https://api.ilanzou.com",
secret: []byte("lanZouY-disk-app"),
bucket: "wpanstore-lanzou",
unproved: "unproved",
proved: "proved",
base: "https://api.ilanzou.com",
secret: []byte("lanZouY-disk-app"),
bucket: "wpanstore-lanzou",
unproved: "unproved",
proved: "proved",
devVersion: "122",
site: "https://www.ilanzou.com",
},
}
})
@ -63,11 +67,13 @@ func init() {
NoOverwriteUpload: false,
},
conf: Conf{
base: "https://api.feijipan.com",
secret: []byte("dingHao-disk-app"),
bucket: "wpanstore",
unproved: "ws",
proved: "app",
base: "https://api.feijipan.com",
secret: []byte("dingHao-disk-app"),
bucket: "wpanstore",
unproved: "ws",
proved: "app",
devVersion: "121",
site: "https://www.feijipan.com",
},
}
})

View File

@ -52,12 +52,16 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
"devType": "6",
"devCode": d.UUID,
"devModel": "chrome",
"devVersion": "120",
"devVersion": d.conf.devVersion,
"appVersion": "",
"timestamp": ts,
//"appToken": d.Token,
"extra": "2",
})
req.SetHeaders(map[string]string{
"Origin": d.conf.site,
"Referer": d.conf.site + "/",
})
if proved {
req.SetQueryParam("appToken", d.Token)
}

View File

@ -62,7 +62,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
for _, file := range dirs {
gateurl := *d.gateURL
gateurl.Path = "ipfs/" + file.Hash
gateurl.RawQuery = "filename=" + file.Name
gateurl.RawQuery = "filename=" + url.PathEscape(file.Name)
objlist = append(objlist, &model.ObjectURL{
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
Url: model.Url{Url: gateurl.String()},
@ -73,7 +73,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
}
func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + url.PathEscape(file.GetName())
return &model.Link{URL: link}, nil
}

View File

@ -206,7 +206,7 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
h := md5.New()
_, err = io.Copy(h, tempFile)
_, err = utils.CopyWithBuffer(h, tempFile)
if err != nil {
return err
}

View File

@ -295,7 +295,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
}
if !initUpdload.FileDataExists {
utils.Log.Error(d.client.CloudDiskStartBusiness())
// utils.Log.Error(d.client.CloudDiskStartBusiness())
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
retry.Attempts(3),
@ -323,6 +323,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
if err != nil {
return err
}
req.ContentLength = byteSize
resp, err := base.HttpClient.Do(req)
if err != nil {
return err

View File

@ -118,6 +118,7 @@ func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
"folder": base.Json{},
"@microsoft.graph.conflictBehavior": "rename",
}
// todo 修复文件夹 ctime/mtime, onedrive 可在 data 里设置 fileSystemInfo 字段, 但是此接口未提供 ctime/mtime
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)

View File

@ -24,12 +24,12 @@ type RespErr struct {
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
Url string `json:"@microsoft.graph.downloadUrl"`
File *struct {
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"`
Url string `json:"@microsoft.graph.downloadUrl"`
File *struct {
MimeType string `json:"mimeType"`
} `json:"file"`
Thumbnails []struct {
@ -58,7 +58,7 @@ func fileToObj(f File, parentID string) *Object {
ID: f.Id,
Name: f.Name,
Size: f.Size,
Modified: f.LastModifiedDateTime,
Modified: f.FileSystemInfo.LastModifiedDateTime,
IsFolder: f.File == nil,
},
Thumbnail: model.Thumbnail{Thumbnail: thumb},
@ -72,3 +72,20 @@ type Files struct {
Value []File `json:"value"`
NextLink string `json:"@odata.nextLink"`
}
// Metadata represents a request to update Metadata.
// It includes only the writeable properties.
// omitempty is intentionally included for all, per https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online#request-body
type Metadata struct {
Description string `json:"description,omitempty"` // Provides a user-visible description of the item. Read-write. Only on OneDrive Personal. Undocumented limit of 1024 characters.
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo,omitempty"` // File system information on client. Read-write.
}
// FileSystemInfoFacet contains properties that are reported by the
// device's local file system for the local version of an item. This
// facet can be used to specify the last modified date or created date
// of the item as it was on the local device.
type FileSystemInfoFacet struct {
CreatedDateTime time.Time `json:"createdDateTime,omitempty"` // The UTC date and time the file was created on a client.
LastModifiedDateTime time.Time `json:"lastModifiedDateTime,omitempty"` // The UTC date and time the file was last modified on a client.
}

View File

@ -127,7 +127,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
func (d *Onedrive) getFiles(path string) ([]File, error) {
var res []File
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
for nextLink != "" {
var files Files
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
@ -148,7 +148,10 @@ func (d *Onedrive) GetFile(path string) (*File, error) {
}
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
filepath := stdpath.Join(dstDir.GetPath(), stream.GetName())
// 1. upload new file
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_put_content?view=odsp-graph-online
url := d.GetMetaUrl(false, filepath) + "/content"
data, err := io.ReadAll(stream)
if err != nil {
return err
@ -156,12 +159,50 @@ func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.F
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
req.SetBody(data).SetContext(ctx)
}, nil)
if err != nil {
return fmt.Errorf("onedrive: Failed to upload new file(path=%v): %w", filepath, err)
}
// 2. update metadata
err = d.updateMetadata(ctx, stream, filepath)
if err != nil {
return fmt.Errorf("onedrive: Failed to update file(path=%v) metadata: %w", filepath, err)
}
return nil
}
func (d *Onedrive) updateMetadata(ctx context.Context, stream model.FileStreamer, filepath string) error {
url := d.GetMetaUrl(false, filepath)
metadata := toAPIMetadata(stream)
// ApiDoc: https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_update?view=odsp-graph-online
_, err := d.Request(url, http.MethodPatch, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
return err
}
func toAPIMetadata(stream model.FileStreamer) Metadata {
metadata := Metadata{
FileSystemInfo: &FileSystemInfoFacet{},
}
if !stream.ModTime().IsZero() {
metadata.FileSystemInfo.LastModifiedDateTime = stream.ModTime()
}
if !stream.CreateTime().IsZero() {
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
}
if stream.CreateTime().IsZero() && !stream.ModTime().IsZero() {
metadata.FileSystemInfo.CreatedDateTime = stream.CreateTime()
}
return metadata
}
func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
res, err := d.Request(url, http.MethodPost, nil, nil)
metadata := map[string]interface{}{"item": toAPIMetadata(stream)}
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(metadata).SetContext(ctx)
}, nil)
if err != nil {
return err
}

View File

@ -4,6 +4,7 @@ import (
"crypto/sha1"
"encoding/hex"
"errors"
"github.com/alist-org/alist/v3/pkg/utils"
"io"
"net/http"
@ -141,7 +142,7 @@ func getGcid(r io.Reader, size int64) (string, error) {
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}

View File

@ -143,7 +143,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
_ = tempFile.Close()
}()
m := md5.New()
_, err = io.Copy(m, tempFile)
_, err = utils.CopyWithBuffer(m, tempFile)
if err != nil {
return err
}
@ -153,7 +153,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
}
md5Str := hex.EncodeToString(m.Sum(nil))
s := sha1.New()
_, err = io.Copy(s, tempFile)
_, err = utils.CopyWithBuffer(s, tempFile)
if err != nil {
return err
}

63
drivers/s3/doge.go Normal file
View File

@ -0,0 +1,63 @@
package s3
import (
"crypto/hmac"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strings"
)
type TmpTokenResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data TmpTokenResponseData `json:"data,omitempty"`
}
type TmpTokenResponseData struct {
Credentials Credentials `json:"Credentials"`
ExpiredAt int `json:"ExpiredAt"`
}
type Credentials struct {
AccessKeyId string `json:"accessKeyId,omitempty"`
SecretAccessKey string `json:"secretAccessKey,omitempty"`
SessionToken string `json:"sessionToken,omitempty"`
}
func getCredentials(AccessKey, SecretKey string) (rst Credentials, err error) {
apiPath := "/auth/tmp_token.json"
reqBody, err := json.Marshal(map[string]interface{}{"channel": "OSS_FULL", "scopes": []string{"*"}})
if err != nil {
return rst, err
}
signStr := apiPath + "\n" + string(reqBody)
hmacObj := hmac.New(sha1.New, []byte(SecretKey))
hmacObj.Write([]byte(signStr))
sign := hex.EncodeToString(hmacObj.Sum(nil))
Authorization := "TOKEN " + AccessKey + ":" + sign
req, err := http.NewRequest("POST", "https://api.dogecloud.com"+apiPath, strings.NewReader(string(reqBody)))
if err != nil {
return rst, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Authorization", Authorization)
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return rst, err
}
defer resp.Body.Close()
ret, err := io.ReadAll(resp.Body)
if err != nil {
return rst, err
}
var tmpTokenResp TmpTokenResponse
err = json.Unmarshal(ret, &tmpTokenResp)
if err != nil {
return rst, err
}
return tmpTokenResp.Data.Credentials, nil
}

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
@ -26,10 +27,13 @@ type S3 struct {
Session *session.Session
client *s3.S3
linkClient *s3.S3
config driver.Config
cron *cron.Cron
}
func (d *S3) Config() driver.Config {
return config
return d.config
}
func (d *S3) GetAddition() driver.Additional {
@ -40,6 +44,18 @@ func (d *S3) Init(ctx context.Context) error {
if d.Region == "" {
d.Region = "alist"
}
if d.config.Name == "Doge" {
// 多吉云每次临时生成的秘钥有效期为 2h所以这里设置为 118 分钟重新生成一次
d.cron = cron.NewCron(time.Minute * 118)
d.cron.Do(func() {
err := d.initSession()
if err != nil {
log.Errorln("Doge init session error:", err)
}
d.client = d.getClient(false)
d.linkClient = d.getClient(true)
})
}
err := d.initSession()
if err != nil {
return err
@ -50,6 +66,9 @@ func (d *S3) Init(ctx context.Context) error {
}
func (d *S3) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil
}

View File

@ -22,15 +22,25 @@ type Addition struct {
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
}
var config = driver.Config{
Name: "S3",
DefaultRoot: "/",
LocalSort: true,
CheckStatus: true,
}
func init() {
op.RegisterDriver(func() driver.Driver {
return &S3{}
return &S3{
config: driver.Config{
Name: "S3",
DefaultRoot: "/",
LocalSort: true,
CheckStatus: true,
},
}
})
op.RegisterDriver(func() driver.Driver {
return &S3{
config: driver.Config{
Name: "Doge",
DefaultRoot: "/",
LocalSort: true,
CheckStatus: true,
},
}
})
}

View File

@ -21,13 +21,21 @@ import (
// do others that not defined in Driver interface
func (d *S3) initSession() error {
var err error
accessKeyID, secretAccessKey, sessionToken := d.AccessKeyID, d.SecretAccessKey, d.SessionToken
if d.config.Name == "Doge" {
credentialsTmp, err := getCredentials(d.AccessKeyID, d.SecretAccessKey)
if err != nil {
return err
}
accessKeyID, secretAccessKey, sessionToken = credentialsTmp.AccessKeyId, credentialsTmp.SecretAccessKey, credentialsTmp.SessionToken
}
cfg := &aws.Config{
Credentials: credentials.NewStaticCredentials(d.AccessKeyID, d.SecretAccessKey, d.SessionToken),
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken),
Region: &d.Region,
Endpoint: &d.Endpoint,
S3ForcePathStyle: aws.Bool(d.ForcePathStyle),
}
var err error
d.Session, err = session.NewSession(cfg)
return err
}

View File

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
@ -19,6 +18,7 @@ type Seafile struct {
Addition
authorization string
libraryMap map[string]*LibraryInfo
}
func (d *Seafile) Config() driver.Config {
@ -31,6 +31,8 @@ func (d *Seafile) GetAddition() driver.Additional {
func (d *Seafile) Init(ctx context.Context) error {
d.Address = strings.TrimSuffix(d.Address, "/")
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
d.libraryMap = make(map[string]*LibraryInfo)
return d.getToken()
}
@ -38,10 +40,37 @@ func (d *Seafile) Drop(ctx context.Context) error {
return nil
}
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {
path := dir.GetPath()
if path == d.RootFolderPath {
libraries, err := d.listLibraries()
if err != nil {
return nil, err
}
if path == "/" && d.RepoId == "" {
return utils.SliceConvert(libraries, func(f LibraryItemResp) (model.Obj, error) {
return &model.Object{
Name: f.Name,
Modified: time.Unix(f.Modified, 0),
Size: f.Size,
IsFolder: true,
}, nil
})
}
}
var repo *LibraryInfo
repo, path, err = d.getRepoAndPath(path)
if err != nil {
return nil, err
}
if repo.Encrypted {
err = d.decryptLibrary(repo)
if err != nil {
return nil, err
}
}
var resp []RepoDirItemResp
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
req.SetResult(&resp).SetQueryParams(map[string]string{
"p": path,
})
@ -63,9 +92,13 @@ func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs)
}
func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(file.GetPath())
if err != nil {
return nil, err
}
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": file.GetPath(),
"p": path,
"reuse": "1",
})
})
@ -78,9 +111,14 @@ func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
}
func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(parentDir.GetPath())
if err != nil {
return err
}
path, _ = utils.JoinBasePath(path, dirName)
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": filepath.Join(parentDir.GetPath(), dirName),
"p": path,
}).SetFormData(map[string]string{
"operation": "mkdir",
})
@ -89,22 +127,34 @@ func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
}
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
if err != nil {
return err
}
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
if err != nil {
return err
}
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": srcObj.GetPath(),
"p": path,
}).SetFormData(map[string]string{
"operation": "move",
"dst_repo": d.Addition.RepoId,
"dst_dir": dstDir.GetPath(),
"dst_repo": dstRepo.Id,
"dst_dir": dstPath,
})
}, true)
return err
}
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
if err != nil {
return err
}
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": srcObj.GetPath(),
"p": path,
}).SetFormData(map[string]string{
"operation": "rename",
"newname": newName,
@ -114,31 +164,47 @@ func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string)
}
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
if err != nil {
return err
}
dstRepo, dstPath, err := d.getRepoAndPath(dstDir.GetPath())
if err != nil {
return err
}
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": srcObj.GetPath(),
"p": path,
}).SetFormData(map[string]string{
"operation": "copy",
"dst_repo": d.Addition.RepoId,
"dst_dir": dstDir.GetPath(),
"dst_repo": dstRepo.Id,
"dst_dir": dstPath,
})
})
return err
}
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
_, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(obj.GetPath())
if err != nil {
return err
}
_, err = d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": obj.GetPath(),
"p": path,
})
})
return err
}
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) {
repo, path, err := d.getRepoAndPath(dstDir.GetPath())
if err != nil {
return err
}
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", repo.Id), func(req *resty.Request) {
req.SetQueryParams(map[string]string{
"p": dstDir.GetPath(),
"p": path,
})
})
if err != nil {
@ -150,7 +216,7 @@ func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
req.SetFileReader("file", stream.GetName(), stream).
SetFormData(map[string]string{
"parent_dir": dstDir.GetPath(),
"parent_dir": path,
"replace": "1",
})
})

View File

@ -9,9 +9,11 @@ type Addition struct {
driver.RootPath
Address string `json:"address" required:"true"`
UserName string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
RepoId string `json:"repoId" required:"true"`
UserName string `json:"username" required:"false"`
Password string `json:"password" required:"false"`
Token string `json:"token" required:"false"`
RepoId string `json:"repoId" required:"false"`
RepoPwd string `json:"repoPwd" required:"false"`
}
var config = driver.Config{

View File

@ -1,14 +1,44 @@
package seafile
import "time"
type AuthTokenResp struct {
Token string `json:"token"`
}
type RepoDirItemResp struct {
type RepoItemResp struct {
Id string `json:"id"`
Type string `json:"type"` // dir, file
Type string `json:"type"` // repo, dir, file
Name string `json:"name"`
Size int64 `json:"size"`
Modified int64 `json:"mtime"`
Permission string `json:"permission"`
}
type LibraryItemResp struct {
RepoItemResp
OwnerContactEmail string `json:"owner_contact_email"`
OwnerName string `json:"owner_name"`
Owner string `json:"owner"`
ModifierEmail string `json:"modifier_email"`
ModifierContactEmail string `json:"modifier_contact_email"`
ModifierName string `json:"modifier_name"`
Virtual bool `json:"virtual"`
MtimeRelative string `json:"mtime_relative"`
Encrypted bool `json:"encrypted"`
Version int `json:"version"`
HeadCommitId string `json:"head_commit_id"`
Root string `json:"root"`
Salt string `json:"salt"`
SizeFormatted string `json:"size_formatted"`
}
type RepoDirItemResp struct {
RepoItemResp
}
type LibraryInfo struct {
LibraryItemResp
decryptedTime time.Time
decryptedSuccess bool
}

View File

@ -1,14 +1,23 @@
package seafile
import (
"errors"
"fmt"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/pkg/utils"
"net/http"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
func (d *Seafile) getToken() error {
if d.Token != "" {
d.authorization = fmt.Sprintf("Token %s", d.Token)
return nil
}
var authResp AuthTokenResp
res, err := base.RestyClient.R().
SetResult(&authResp).
@ -60,3 +69,110 @@ func (d *Seafile) request(method string, pathname string, callback base.ReqCallb
}
return res.Body(), nil
}
func (d *Seafile) getRepoAndPath(fullPath string) (repo *LibraryInfo, path string, err error) {
libraryMap := d.libraryMap
repoId := d.Addition.RepoId
if repoId != "" {
if len(repoId) == 36 /* uuid */ {
for _, library := range libraryMap {
if library.Id == repoId {
return library, fullPath, nil
}
}
}
} else {
var repoName string
str := fullPath[1:]
pos := strings.IndexRune(str, '/')
if pos == -1 {
repoName = str
} else {
repoName = str[:pos]
}
path = utils.FixAndCleanPath(fullPath[1+len(repoName):])
if library, ok := libraryMap[repoName]; ok {
return library, path, nil
}
}
return nil, "", errs.ObjectNotFound
}
func (d *Seafile) listLibraries() (resp []LibraryItemResp, err error) {
repoId := d.Addition.RepoId
if repoId == "" {
_, err = d.request(http.MethodGet, "/api2/repos/", func(req *resty.Request) {
req.SetResult(&resp)
})
} else {
var oneResp LibraryItemResp
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/", repoId), func(req *resty.Request) {
req.SetResult(&oneResp)
})
if err == nil {
resp = append(resp, oneResp)
}
}
if err != nil {
return nil, err
}
libraryMap := make(map[string]*LibraryInfo)
var putLibraryMap func(library LibraryItemResp, index int)
putLibraryMap = func(library LibraryItemResp, index int) {
name := library.Name
if index > 0 {
name = fmt.Sprintf("%s (%d)", name, index)
}
if _, exist := libraryMap[name]; exist {
putLibraryMap(library, index+1)
} else {
libraryInfo := LibraryInfo{}
data, _ := utils.Json.Marshal(library)
_ = utils.Json.Unmarshal(data, &libraryInfo)
libraryMap[name] = &libraryInfo
}
}
for _, library := range resp {
putLibraryMap(library, 0)
}
d.libraryMap = libraryMap
return resp, nil
}
var repoPwdNotConfigured = errors.New("library password not configured")
var repoPwdIncorrect = errors.New("library password is incorrect")
func (d *Seafile) decryptLibrary(repo *LibraryInfo) (err error) {
if !repo.Encrypted {
return nil
}
if d.RepoPwd == "" {
return repoPwdNotConfigured
}
now := time.Now()
decryptedTime := repo.decryptedTime
if repo.decryptedSuccess {
if now.Sub(decryptedTime).Minutes() <= 30 {
return nil
}
} else {
if now.Sub(decryptedTime).Seconds() <= 10 {
return repoPwdIncorrect
}
}
var resp string
_, err = d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/", repo.Id), func(req *resty.Request) {
req.SetResult(&resp).SetFormData(map[string]string{
"password": d.RepoPwd,
})
})
repo.decryptedTime = time.Now()
if err != nil || !strings.Contains(resp, "success") {
repo.decryptedSuccess = false
return err
}
repo.decryptedSuccess = true
return nil
}

View File

@ -1,7 +1,7 @@
package smb
import (
"io"
"github.com/alist-org/alist/v3/pkg/utils"
"io/fs"
"net"
"os"
@ -74,7 +74,7 @@ func (d *SMB) CopyFile(src, dst string) error {
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
if _, err = utils.CopyWithBuffer(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = d.fs.Stat(src); err != nil {

View File

@ -190,7 +190,7 @@ func getGcid(r io.Reader, size int64) (string, error) {
readSize := calcBlockSize(size)
for {
hash2.Reset()
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
if n, err := utils.CopyWithBufferN(hash2, r, readSize); err != nil && n == 0 {
if err != io.EOF {
return "", err
}

26
go.mod
View File

@ -3,6 +3,7 @@ module github.com/alist-org/alist/v3
go 1.21
require (
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700
github.com/SheltonZhu/115driver v1.0.22
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
github.com/Xhofe/rateg v0.0.0-20230728072201-251a4e1adad4
@ -39,6 +40,7 @@ require (
github.com/meilisearch/meilisearch-go v0.26.1
github.com/minio/sio v0.3.0
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/ncw/swift/v2 v2.0.2
github.com/orzogc/fake115uploader v0.3.3-0.20230715111618-58f9eb76f831
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.6
@ -47,15 +49,15 @@ require (
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.8.4
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7
github.com/u2takey/ffmpeg-go v0.5.0
github.com/upyun/go-sdk/v3 v3.0.4
github.com/winfsp/cgofuse v1.5.1-0.20230130140708-f87f5db493b5
github.com/xhofe/tache v0.1.1
golang.org/x/crypto v0.18.0
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e
golang.org/x/crypto v0.19.0
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225
golang.org/x/image v0.15.0
golang.org/x/net v0.20.0
golang.org/x/net v0.21.0
golang.org/x/oauth2 v0.16.0
golang.org/x/time v0.5.0
google.golang.org/appengine v1.6.8
@ -137,8 +139,8 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jzelinskie/whirlpool v0.0.0-20201016144138-0675e54bb004 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
@ -153,7 +155,7 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -172,7 +174,6 @@ require (
github.com/multiformats/go-multihash v0.2.3 // indirect
github.com/multiformats/go-multistream v0.4.1 // indirect
github.com/multiformats/go-varint v0.0.7 // indirect
github.com/ncw/swift/v2 v2.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
@ -184,6 +185,8 @@ require (
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rfjakob/eme v1.1.2 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
@ -201,10 +204,11 @@ require (
github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
golang.org/x/arch v0.5.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/term v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/term v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.18.0 // indirect
google.golang.org/api v0.134.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect
google.golang.org/grpc v1.57.0 // indirect

52
go.sum
View File

@ -7,6 +7,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd h1:nzE1YQBdx1bq9IlZinHa+HVffy+NmVRoKr+wHN8fpLE=
github.com/Max-Sum/base32768 v0.0.0-20230304063302-18e6ce5945fd/go.mod h1:C8yoIfvESpM3GD07OCHU7fqI7lhwyZ2Td1rbNbTAhnc=
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700 h1:r3fp2/Ro+0RtpjNY0/wsbN7vRmCW//dXTOZDQTct25Q=
github.com/Mikubill/gofakes3 v0.0.3-0.20230622102024-284c0f988700/go.mod h1:OSXqXEGUe9CmPiwLMMnVrbXonMf4BeLBkBdLufxxiyY=
github.com/RoaringBitmap/roaring v1.2.3 h1:yqreLINqIrX22ErkKI0vY47/ivtJr6n+kMhVOVmhWBY=
github.com/RoaringBitmap/roaring v1.2.3/go.mod h1:plvDsJQpxOC5bw8LRteu/MLWHsHez/3y6cubLI4/1yE=
github.com/SheltonZhu/115driver v1.0.22 h1:Wp8pN7/gK3YwEO5P18ggbIOHM++lo9eP/pBhuvXfI6U=
@ -32,8 +34,6 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.38.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
github.com/aws/aws-sdk-go v1.49.18 h1:g/iMXkfXeJQ7MvnLwroxWsTTNkHtdVJGxIgrAIEG62M=
github.com/aws/aws-sdk-go v1.49.18/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.50.24 h1:3o2Pg7mOoVL0jv54vWtuafoZqAeEXLhm1tltWA2GcEw=
github.com/aws/aws-sdk-go v1.50.24/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@ -262,12 +262,11 @@ github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQL
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.6/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI=
github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@ -315,8 +314,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/meilisearch/meilisearch-go v0.26.1 h1:3bmo2uLijX7kvBmiZ9LupVfC95TFcRJDgrRTzbOoE4A=
github.com/meilisearch/meilisearch-go v0.26.1/go.mod h1:SxuSqDcPBIykjWz1PX+KzsYzArNLSCadQodWs8extS0=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/minio/sio v0.3.0 h1:syEFBewzOMOYVzSTFpp1MqpSZk8rUNbz8VIIc+PNzus=
github.com/minio/sio v0.3.0/go.mod h1:8b0yPp2avGThviy/+OCJBI6OMpvxoUuiLvE6F1lebhw=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
@ -408,6 +407,10 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI=
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0=
github.com/shirou/gopsutil/v3 v3.23.7 h1:C+fHO8hfIppoJ1WdsVm1RoI0RwXoNdfTK7yWXV0wVj4=
github.com/shirou/gopsutil/v3 v3.23.7/go.mod h1:c4gnmoRC0hQuaLqvxnx1//VXQ0Ms/X9UnJF8pddY5z4=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
@ -444,8 +447,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca h1:I9rVnNXdIkij4UvMT7OmKhH9sOIvS8iXkxfPdnn9wQA=
github.com/t3rm1n4l/go-mega v0.0.0-20230228171823-a01a2cda13ca/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7 h1:Jtcrb09q0AVWe3BGe8qtuuGxNSHWGkTWr43kHTJ+CpA=
github.com/t3rm1n4l/go-mega v0.0.0-20240219080617-d494b6a8ace7/go.mod h1:suDIky6yrK07NnaBadCB4sS0CqFOvUK91lH7CR+JlDA=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
@ -502,10 +505,10 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE=
golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
@ -523,16 +526,16 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -560,16 +563,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -589,9 +592,12 @@ golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190829051458-42f498d34c4d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.134.0 h1:ktL4Goua+UBgoP1eL1/60LwZJqa1sIzkLmvoR3hR6Gw=

View File

@ -1,6 +1,7 @@
package authn
import (
"fmt"
"net/http"
"net/url"
@ -19,7 +20,7 @@ func NewAuthnInstance(r *http.Request) (*webauthn.WebAuthn, error) {
RPDisplayName: setting.GetStr(conf.SiteTitle),
RPID: siteUrl.Hostname(),
//RPOrigin: siteUrl.String(),
RPOrigins: []string{siteUrl.String()},
RPOrigins: []string{fmt.Sprintf("%s://%s", siteUrl.Scheme, siteUrl.Host)},
// RPOrigin: "http://localhost:5173"
})
}

View File

@ -21,7 +21,6 @@ func initSettings() {
if err != nil {
utils.Log.Fatalf("failed get settings: %+v", err)
}
for i := range settings {
if !isActive(settings[i].Key) && settings[i].Flag != model.DEPRECATED {
settings[i].Flag = model.DEPRECATED
@ -35,6 +34,9 @@ func initSettings() {
// create or save setting
for i := range initialSettingItems {
item := &initialSettingItems[i]
if item.PreDefault == "" {
item.PreDefault = item.Value
}
// err
stored, err := op.GetSettingItemByKey(item.Key)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@ -42,7 +44,7 @@ func initSettings() {
continue
}
// save
if stored != nil && item.Key != conf.VERSION {
if stored != nil && item.Key != conf.VERSION && stored.Value != item.PreDefault {
item.Value = stored.Value
}
if stored == nil || *item != *stored {
@ -98,7 +100,7 @@ func InitialSettings() []model.SettingItem {
// preview settings
{Key: conf.TextTypes, Value: "txt,htm,html,xml,java,properties,sql,js,md,json,conf,ini,vue,php,py,bat,gitignore,yml,go,sh,c,cpp,h,hpp,tsx,vtt,srt,ass,rs,lrc", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.AudioTypes, Value: "mp3,flac,ogg,m4a,wav,opus,wma", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.VideoTypes, Value: "mp4,mkv,avi,mov,rmvb,webm,flv,m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.ImageTypes, Value: "jpg,tiff,jpeg,png,gif,bmp,svg,ico,swf,webp", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
//{Key: conf.OfficeTypes, Value: "doc,docx,xls,xlsx,ppt,pptx", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
{Key: conf.ProxyTypes, Value: "m3u8", Type: conf.TypeText, Group: model.PREVIEW, Flag: model.PRIVATE},
@ -129,7 +131,7 @@ func InitialSettings() []model.SettingItem {
// global settings
{Key: conf.HideFiles, Value: "/\\/README.md/i", Type: conf.TypeText, Group: model.GLOBAL},
{Key: "package_download", Value: "true", Type: conf.TypeBool, Group: model.GLOBAL},
{Key: conf.CustomizeHead, Value: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.CustomizeHead, PreDefault: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.LinkExpiration, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.SignAll, Value: "true", Type: conf.TypeBool, Group: model.GLOBAL, Flag: model.PRIVATE},
@ -176,6 +178,11 @@ func InitialSettings() []model.SettingItem {
{Key: conf.LdapDefaultDir, Value: "/", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},
{Key: conf.LdapDefaultPermission, Value: "0", Type: conf.TypeNumber, Group: model.LDAP, Flag: model.PRIVATE},
{Key: conf.LdapLoginTips, Value: "login with ldap", Type: conf.TypeString, Group: model.LDAP, Flag: model.PUBLIC},
//s3 settings
{Key: conf.S3AccessKeyId, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
{Key: conf.S3SecretAccessKey, Value: "", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
{Key: conf.S3Buckets, Value: "[]", Type: conf.TypeString, Group: model.S3, Flag: model.PRIVATE},
}
initialSettingItems = append(initialSettingItems, tool.Tools.Items()...)
if flags.Dev {

View File

@ -31,6 +31,7 @@ func initUser() {
PwdHash: model.TwoHashPwd(adminPassword, salt),
Role: model.ADMIN,
BasePath: "/",
Authn: "[]",
}
if err := op.CreateUser(admin); err != nil {
panic(err)
@ -53,6 +54,7 @@ func initUser() {
BasePath: "/",
Permission: 0,
Disabled: true,
Authn: "[]",
}
if err := db.CreateUser(guest); err != nil {
utils.Log.Fatalf("[init user] Failed to create guest user: %v", err)
@ -62,6 +64,7 @@ func initUser() {
}
}
hashPwdForOldVersion()
updateAuthnForOldVersion()
}
func hashPwdForOldVersion() {
@ -80,3 +83,19 @@ func hashPwdForOldVersion() {
}
}
}
func updateAuthnForOldVersion() {
users, _, err := op.GetUsers(1, -1)
if err != nil {
utils.Log.Fatalf("[update authn for old version] failed get users: %v", err)
}
for i := range users {
user := users[i]
if user.Authn == "" {
user.Authn = "[]"
if err := db.UpdateUser(&user); err != nil {
utils.Log.Fatalf("[update authn for old version] failed update user: %v", err)
}
}
}
}

View File

@ -21,8 +21,8 @@ func LoadStorages() {
if err != nil {
utils.Log.Errorf("failed get enabled storages: %+v", err)
} else {
utils.Log.Infof("success load storage: [%s], driver: [%s]",
storages[i].MountPath, storages[i].Driver)
utils.Log.Infof("success load storage: [%s], driver: [%s], order: [%d]",
storages[i].MountPath, storages[i].Driver, storages[i].Order)
}
}
conf.StoragesLoaded = true

View File

@ -1,9 +1,10 @@
package conf
import (
"path/filepath"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/pkg/utils/random"
"path/filepath"
)
type Database struct {
@ -63,6 +64,12 @@ type Cors struct {
AllowHeaders []string `json:"allow_headers" env:"ALLOW_HEADERS"`
}
type S3 struct {
Enable bool `json:"enable" env:"ENABLE"`
Port int `json:"port" env:"PORT"`
SSL bool `json:"ssl" env:"SSL"`
}
type Config struct {
Force bool `json:"force" env:"FORCE"`
SiteURL string `json:"site_url" env:"SITE_URL"`
@ -70,7 +77,7 @@ type Config struct {
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database" envPrefix:"DB_"`
Meilisearch Meilisearch `json:"meilisearch" env:"MEILISEARCH"`
Meilisearch Meilisearch `json:"meilisearch" envPrefix:"MEILISEARCH_"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
BleveDir string `json:"bleve_dir" env:"BLEVE_DIR"`
@ -81,6 +88,7 @@ type Config struct {
TlsInsecureSkipVerify bool `json:"tls_insecure_skip_verify" env:"TLS_INSECURE_SKIP_VERIFY"`
Tasks TasksConfig `json:"tasks" envPrefix:"TASKS_"`
Cors Cors `json:"cors" envPrefix:"CORS_"`
S3 S3 `json:"s3" envPrefix:"S3_"`
}
func DefaultConfig() *Config {
@ -142,5 +150,10 @@ func DefaultConfig() *Config {
AllowMethods: []string{"*"},
AllowHeaders: []string{"*"},
},
S3: S3{
Enable: false,
Port: 5246,
SSL: false,
},
}
}

View File

@ -84,6 +84,11 @@ const (
LdapDefaultDir = "ldap_default_dir"
LdapLoginTips = "ldap_login_tips"
//s3
S3Buckets = "s3_buckets"
S3AccessKeyId = "s3_access_key_id"
S3SecretAccessKey = "s3_secret_access_key"
// qbittorrent
QbittorrentUrl = "qbittorrent_url"
QbittorrentSeedtime = "qbittorrent_seedtime"

View File

@ -2,6 +2,7 @@ package db
import (
"fmt"
"sort"
"github.com/alist-org/alist/v3/internal/model"
"github.com/pkg/errors"
@ -65,5 +66,8 @@ func GetEnabledStorages() ([]model.Storage, error) {
if err := db.Where(fmt.Sprintf("%s = ?", columnName("disabled")), false).Find(&storages).Error; err != nil {
return nil, errors.WithStack(err)
}
sort.Slice(storages, func(i, j int) bool {
return storages[i].Order < storages[j].Order
})
return storages, nil
}

View File

@ -41,6 +41,7 @@ type FileStreamer interface {
GetMimetype() string
//SetReader(io.Reader)
NeedStore() bool
IsForceStreamUpload() bool
GetExist() Obj
SetExist(Obj)
//for a non-seekable Stream, RangeRead supports peeking some data, and CacheFullInTempFile still works

View File

@ -10,6 +10,7 @@ const (
INDEX
SSO
LDAP
S3
)
const (
@ -20,13 +21,14 @@ const (
)
type SettingItem struct {
Key string `json:"key" gorm:"primaryKey" binding:"required"` // unique key
Value string `json:"value"` // value
Help string `json:"help"` // help message
Type string `json:"type"` // string, number, bool, select
Options string `json:"options"` // values for select
Group int `json:"group"` // use to group setting in frontend
Flag int `json:"flag"` // 0 = public, 1 = private, 2 = readonly, 3 = deprecated, etc.
Key string `json:"key" gorm:"primaryKey" binding:"required"` // unique key
Value string `json:"value"` // value
PreDefault string `json:"-" gorm:"-:all"` // deprecated value
Help string `json:"help"` // help message
Type string `json:"type"` // string, number, bool, select
Options string `json:"options"` // values for select
Group int `json:"group"` // use to group setting in frontend
Flag int `json:"flag"` // 0 = public, 1 = private, 2 = readonly, 3 = deprecated, etc.
}
func (s SettingItem) IsDeprecated() bool {

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"io"
"math"
"net/http"
@ -271,7 +272,7 @@ func (d *downloader) tryDownloadChunk(params *HttpRequestParams, ch *chunk) (int
}
}
n, err := io.Copy(ch.buf, resp.Body)
n, err := utils.CopyWithBuffer(ch.buf, resp.Body)
if err != nil {
return n, &errReadingBody{err: err}

View File

@ -162,7 +162,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
pw.CloseWithError(err)
return
}
if _, err := io.CopyN(part, reader, ra.Length); err != nil {
if _, err := utils.CopyWithBufferN(part, reader, ra.Length); err != nil {
pw.CloseWithError(err)
return
}
@ -182,7 +182,7 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request, name string, modTime time
w.WriteHeader(code)
if r.Method != "HEAD" {
written, err := io.CopyN(w, sendContent, sendSize)
written, err := utils.CopyWithBufferN(w, sendContent, sendSize)
if err != nil {
log.Warnf("ServeHttp error. err: %s ", err)
if written != sendSize {

View File

@ -2,6 +2,7 @@ package net
import (
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"io"
"math"
"mime/multipart"
@ -327,10 +328,10 @@ func GetRangedHttpReader(readCloser io.ReadCloser, offset, length int64) (io.Rea
length_int = int(length)
if offset > 100*1024*1024 {
log.Warnf("offset is more than 100MB, if loading data from internet, high-latency and wasting of bandwith is expected")
log.Warnf("offset is more than 100MB, if loading data from internet, high-latency and wasting of bandwidth is expected")
}
if _, err := io.Copy(io.Discard, io.LimitReader(readCloser, offset)); err != nil {
if _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(readCloser, offset)); err != nil {
return nil, err
}

View File

@ -18,9 +18,10 @@ type FileStream struct {
Ctx context.Context
model.Obj
io.Reader
Mimetype string
WebPutAsTask bool
Exist model.Obj //the file existed in the destination, we can reuse some info since we wil overwrite it
Mimetype string
WebPutAsTask bool
ForceStreamUpload bool
Exist model.Obj //the file existed in the destination, we can reuse some info since we wil overwrite it
utils.Closers
tmpFile *os.File //if present, tmpFile has full content, it will be deleted at last
peekBuff *bytes.Reader
@ -43,6 +44,11 @@ func (f *FileStream) GetMimetype() string {
func (f *FileStream) NeedStore() bool {
return f.WebPutAsTask
}
func (f *FileStream) IsForceStreamUpload() bool {
return f.ForceStreamUpload
}
func (f *FileStream) Close() error {
var err1, err2 error
err1 = f.Closers.Close()
@ -98,7 +104,7 @@ func (f *FileStream) RangeRead(httpRange http_range.Range) (io.Reader, error) {
if httpRange.Start == 0 && httpRange.Length <= InMemoryBufMaxSizeBytes && f.peekBuff == nil {
bufSize := utils.Min(httpRange.Length, f.GetSize())
newBuf := bytes.NewBuffer(make([]byte, 0, bufSize))
n, err := io.CopyN(newBuf, f.Reader, bufSize)
n, err := utils.CopyWithBufferN(newBuf, f.Reader, bufSize)
if err != nil {
return nil, err
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"encoding/xml"
"fmt"
"github.com/alist-org/alist/v3/pkg/utils"
"io"
"net/http"
"net/url"
@ -419,7 +420,7 @@ func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadClos
// stream in rs.Body
if rs.StatusCode == 200 {
// discard first 'offset' bytes.
if _, err := io.Copy(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
if _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
return nil, newPathErrorErr("ReadStreamRange", path, err)
}

View File

@ -32,7 +32,7 @@ func CopyFile(src, dst string) error {
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
if _, err = CopyWithBuffer(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
@ -121,7 +121,7 @@ func CreateTempFile(r io.Reader, size int64) (*os.File, error) {
if err != nil {
return nil, err
}
readBytes, err := io.Copy(f, r)
readBytes, err := CopyWithBuffer(f, r)
if err != nil {
_ = os.Remove(f.Name())
return nil, errs.NewErr(err, "CreateTempFile failed")

View File

@ -96,7 +96,7 @@ func HashData(hashType *HashType, data []byte, params ...any) string {
// HashReader get hash of one hashType from a reader
func HashReader(hashType *HashType, reader io.Reader, params ...any) (string, error) {
h := hashType.NewFunc(params...)
_, err := io.Copy(h, reader)
_, err := CopyWithBuffer(h, reader)
if err != nil {
return "", errs.NewErr(err, "HashReader error")
}

View File

@ -4,7 +4,6 @@ import (
"bytes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io"
"testing"
)
@ -36,7 +35,7 @@ var hashTestSet = []hashTest{
func TestMultiHasher(t *testing.T) {
for _, test := range hashTestSet {
mh := NewMultiHasher([]*HashType{MD5, SHA1, SHA256})
n, err := io.Copy(mh, bytes.NewBuffer(test.input))
n, err := CopyWithBuffer(mh, bytes.NewBuffer(test.input))
require.NoError(t, err)
assert.Len(t, test.input, int(n))
hashInfo := mh.GetHashInfo()

View File

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"sync"
"time"
"golang.org/x/exp/constraints"
@ -29,7 +30,7 @@ func CopyWithCtx(ctx context.Context, out io.Writer, in io.Reader, size int64, p
// possible in the call process.
var finish int64 = 0
s := size / 100
_, err := io.Copy(out, readerFunc(func(p []byte) (int, error) {
_, err := CopyWithBuffer(out, readerFunc(func(p []byte) (int, error) {
// golang non-blocking channel: https://gobyexample.com/non-blocking-channel-operations
select {
// if context has been canceled
@ -204,3 +205,31 @@ func Max[T constraints.Ordered](a, b T) T {
}
return a
}
var IoBuffPool = &sync.Pool{
New: func() interface{} {
return make([]byte, 32*1024*2) // Two times of size in io package
},
}
func CopyWithBuffer(dst io.Writer, src io.Reader) (written int64, err error) {
buff := IoBuffPool.Get().([]byte)
defer IoBuffPool.Put(buff)
written, err = io.CopyBuffer(dst, src, buff)
if err != nil {
return
}
return written, nil
}
func CopyWithBufferN(dst io.Writer, src io.Reader, n int64) (written int64, err error) {
written, err = CopyWithBuffer(dst, io.LimitReader(src, n))
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
err = io.EOF
}
return
}

View File

@ -37,3 +37,28 @@ func NewDebounce2(interval time.Duration, f func()) func() {
(*time.Timer)(timer).Reset(interval)
}
}
func NewThrottle(interval time.Duration) func(func()) {
var lastCall time.Time
return func(fn func()) {
now := time.Now()
if now.Sub(lastCall) < interval {
return
}
time.AfterFunc(interval, fn)
lastCall = now
}
}
func NewThrottle2(interval time.Duration, fn func()) func() {
var lastCall time.Time
return func() {
now := time.Now()
if now.Sub(lastCall) < interval {
return
}
time.AfterFunc(interval, fn)
lastCall = now
}
}

View File

@ -398,7 +398,7 @@ func SSOLoginCallback(c *gin.Context) {
}
userID := utils.Json.Get(resp.Body(), idField).ToString()
if utils.SliceContains([]string{"", "0"}, userID) {
common.ErrorResp(c, errors.New("error occured"), 400)
common.ErrorResp(c, errors.New("error occurred"), 400)
return
}
if argument == "get_sso_id" {

View File

@ -41,6 +41,7 @@ func CreateUser(c *gin.Context) {
}
req.SetPassword(req.Password)
req.Password = ""
req.Authn = "[]"
if err := op.CreateUser(&req); err != nil {
common.ErrorResp(c, err, 500, true)
} else {

View File

@ -36,6 +36,7 @@ func Init(e *gin.Engine) {
g.Use(middlewares.MaxAllowed(conf.Conf.MaxConnections))
}
WebDav(g.Group("/dav"))
S3(g.Group("/s3"))
g.GET("/d/*path", middlewares.Down, handles.Down)
g.GET("/p/*path", middlewares.Down, handles.Proxy)
@ -170,3 +171,8 @@ func Cors(r *gin.Engine) {
config.AllowMethods = conf.Conf.Cors.AllowMethods
r.Use(cors.New(config))
}
func InitS3(e *gin.Engine) {
Cors(e)
S3Server(e.Group("/"))
}

39
server/s3.go Normal file
View File

@ -0,0 +1,39 @@
package server
import (
"context"
"path"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/s3"
"github.com/gin-gonic/gin"
)
func S3(g *gin.RouterGroup) {
if !conf.Conf.S3.Enable {
g.Any("/*path", func(c *gin.Context) {
common.ErrorStrResp(c, "S3 server is not enabled", 403)
})
return
}
if conf.Conf.S3.Port != -1 {
g.Any("/*path", func(c *gin.Context) {
common.ErrorStrResp(c, "S3 server bound to single port", 403)
})
return
}
h, _ := s3.NewServer(context.Background())
g.Any("/*path", func(c *gin.Context) {
adjustedPath := strings.TrimPrefix(c.Request.URL.Path, path.Join(conf.URL.Path, "/s3"))
c.Request.URL.Path = adjustedPath
gin.WrapH(h)(c)
})
}
func S3Server(g *gin.RouterGroup) {
h, _ := s3.NewServer(context.Background())
g.Any("/*path", gin.WrapH(h))
}

432
server/s3/backend.go Normal file
View File

@ -0,0 +1,432 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"context"
"encoding/hex"
"fmt"
"io"
"path"
"strings"
"sync"
"time"
"github.com/Mikubill/gofakes3"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/stream"
"github.com/alist-org/alist/v3/pkg/http_range"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/ncw/swift/v2"
)
var (
emptyPrefix = &gofakes3.Prefix{}
timeFormat = "Mon, 2 Jan 2006 15:04:05.999999999 GMT"
)
// s3Backend implements the gofacess3.Backend interface to make an S3
// backend for gofakes3
type s3Backend struct {
meta *sync.Map
}
// newBackend creates a new SimpleBucketBackend.
func newBackend() gofakes3.Backend {
return &s3Backend{
meta: new(sync.Map),
}
}
// ListBuckets always returns the default bucket.
func (b *s3Backend) ListBuckets() ([]gofakes3.BucketInfo, error) {
buckets, err := getAndParseBuckets()
if err != nil {
return nil, err
}
var response []gofakes3.BucketInfo
ctx := context.Background()
for _, b := range buckets {
node, _ := fs.Get(ctx, b.Path, &fs.GetArgs{})
response = append(response, gofakes3.BucketInfo{
// Name: gofakes3.URLEncode(b.Name),
Name: b.Name,
CreationDate: gofakes3.NewContentTime(node.ModTime()),
})
}
return response, nil
}
// ListBucket lists the objects in the given bucket.
func (b *s3Backend) ListBucket(bucketName string, prefix *gofakes3.Prefix, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {
bucket, err := getBucketByName(bucketName)
if err != nil {
return nil, err
}
bucketPath := bucket.Path
if prefix == nil {
prefix = emptyPrefix
}
// workaround
if strings.TrimSpace(prefix.Prefix) == "" {
prefix.HasPrefix = false
}
if strings.TrimSpace(prefix.Delimiter) == "" {
prefix.HasDelimiter = false
}
response := gofakes3.NewObjectList()
path, remaining := prefixParser(prefix)
err = b.entryListR(bucketPath, path, remaining, prefix.HasDelimiter, response)
if err == gofakes3.ErrNoSuchKey {
// AWS just returns an empty list
response = gofakes3.NewObjectList()
} else if err != nil {
return nil, err
}
return b.pager(response, page)
}
// HeadObject returns the fileinfo for the given object name.
//
// Note that the metadata is not supported yet.
func (b *s3Backend) HeadObject(bucketName, objectName string) (*gofakes3.Object, error) {
ctx := context.Background()
bucket, err := getBucketByName(bucketName)
if err != nil {
return nil, err
}
bucketPath := bucket.Path
fp := path.Join(bucketPath, objectName)
fmeta, _ := op.GetNearestMeta(fp)
node, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{})
if err != nil {
return nil, gofakes3.KeyNotFound(objectName)
}
if node.IsDir() {
return nil, gofakes3.KeyNotFound(objectName)
}
size := node.GetSize()
// hash := getFileHashByte(fobj)
meta := map[string]string{
"Last-Modified": node.ModTime().Format(timeFormat),
"Content-Type": utils.GetMimeType(fp),
}
if val, ok := b.meta.Load(fp); ok {
metaMap := val.(map[string]string)
for k, v := range metaMap {
meta[k] = v
}
}
return &gofakes3.Object{
Name: objectName,
// Hash: hash,
Metadata: meta,
Size: size,
Contents: noOpReadCloser{},
}, nil
}
// GetObject fetchs the object from the filesystem.
func (b *s3Backend) GetObject(bucketName, objectName string, rangeRequest *gofakes3.ObjectRangeRequest) (obj *gofakes3.Object, err error) {
ctx := context.Background()
bucket, err := getBucketByName(bucketName)
if err != nil {
return nil, err
}
bucketPath := bucket.Path
fp := path.Join(bucketPath, objectName)
fmeta, _ := op.GetNearestMeta(fp)
node, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{})
if err != nil {
return nil, gofakes3.KeyNotFound(objectName)
}
if node.IsDir() {
return nil, gofakes3.KeyNotFound(objectName)
}
link, file, err := fs.Link(ctx, fp, model.LinkArgs{})
if err != nil {
return nil, err
}
size := file.GetSize()
rnge, err := rangeRequest.Range(size)
if err != nil {
return nil, err
}
if link.RangeReadCloser == nil && link.MFile == nil && len(link.URL) == 0 {
return nil, fmt.Errorf("the remote storage driver need to be enhanced to support s3")
}
remoteFileSize := file.GetSize()
remoteClosers := utils.EmptyClosers()
rangeReaderFunc := func(ctx context.Context, start, length int64) (io.ReadCloser, error) {
if length >= 0 && start+length >= remoteFileSize {
length = -1
}
rrc := link.RangeReadCloser
if len(link.URL) > 0 {
rangedRemoteLink := &model.Link{
URL: link.URL,
Header: link.Header,
}
var converted, err = stream.GetRangeReadCloserFromLink(remoteFileSize, rangedRemoteLink)
if err != nil {
return nil, err
}
rrc = converted
}
if rrc != nil {
remoteReader, err := rrc.RangeRead(ctx, http_range.Range{Start: start, Length: length})
remoteClosers.AddClosers(rrc.GetClosers())
if err != nil {
return nil, err
}
return remoteReader, nil
}
if link.MFile != nil {
_, err := link.MFile.Seek(start, io.SeekStart)
if err != nil {
return nil, err
}
//remoteClosers.Add(remoteLink.MFile)
//keep reuse same MFile and close at last.
remoteClosers.Add(link.MFile)
return io.NopCloser(link.MFile), nil
}
return nil, errs.NotSupport
}
var rdr io.ReadCloser
if rnge != nil {
rdr, err = rangeReaderFunc(ctx, rnge.Start, rnge.Length)
if err != nil {
return nil, err
}
} else {
rdr, err = rangeReaderFunc(ctx, 0, -1)
if err != nil {
return nil, err
}
}
meta := map[string]string{
"Last-Modified": node.ModTime().Format(timeFormat),
"Content-Type": utils.GetMimeType(fp),
}
if val, ok := b.meta.Load(fp); ok {
metaMap := val.(map[string]string)
for k, v := range metaMap {
meta[k] = v
}
}
return &gofakes3.Object{
// Name: gofakes3.URLEncode(objectName),
Name: objectName,
// Hash: "",
Metadata: meta,
Size: size,
Range: rnge,
Contents: rdr,
}, nil
}
// TouchObject creates or updates meta on specified object.
func (b *s3Backend) TouchObject(fp string, meta map[string]string) (result gofakes3.PutObjectResult, err error) {
//TODO: implement
return result, gofakes3.ErrNotImplemented
}
// PutObject creates or overwrites the object with the given name.
func (b *s3Backend) PutObject(
bucketName, objectName string,
meta map[string]string,
input io.Reader, size int64,
) (result gofakes3.PutObjectResult, err error) {
ctx := context.Background()
bucket, err := getBucketByName(bucketName)
if err != nil {
return result, err
}
bucketPath := bucket.Path
fp := path.Join(bucketPath, objectName)
reqPath := path.Dir(fp)
fmeta, _ := op.GetNearestMeta(fp)
_, err = fs.Get(context.WithValue(ctx, "meta", fmeta), reqPath, &fs.GetArgs{})
if err != nil {
return result, gofakes3.KeyNotFound(objectName)
}
var ti time.Time
if val, ok := meta["X-Amz-Meta-Mtime"]; ok {
ti, _ = swift.FloatStringToTime(val)
}
if val, ok := meta["mtime"]; ok {
ti, _ = swift.FloatStringToTime(val)
}
obj := model.Object{
Name: path.Base(fp),
Size: size,
Modified: ti,
Ctime: time.Now(),
}
stream := &stream.FileStream{
Obj: &obj,
Reader: input,
Mimetype: meta["Content-Type"],
}
err = fs.PutDirectly(ctx, reqPath, stream)
if err != nil {
return result, err
}
if err := stream.Close(); err != nil {
// remove file when close error occurred (FsPutErr)
_ = fs.Remove(ctx, fp)
return result, err
}
b.meta.Store(fp, meta)
return result, nil
}
// DeleteMulti deletes multiple objects in a single request.
func (b *s3Backend) DeleteMulti(bucketName string, objects ...string) (result gofakes3.MultiDeleteResult, rerr error) {
for _, object := range objects {
if err := b.deleteObject(bucketName, object); err != nil {
utils.Log.Errorf("serve s3", "delete object failed: %v", err)
result.Error = append(result.Error, gofakes3.ErrorResult{
Code: gofakes3.ErrInternal,
Message: gofakes3.ErrInternal.Message(),
Key: object,
})
} else {
result.Deleted = append(result.Deleted, gofakes3.ObjectID{
Key: object,
})
}
}
return result, nil
}
// DeleteObject deletes the object with the given name.
func (b *s3Backend) DeleteObject(bucketName, objectName string) (result gofakes3.ObjectDeleteResult, rerr error) {
return result, b.deleteObject(bucketName, objectName)
}
// deleteObject deletes the object from the filesystem.
func (b *s3Backend) deleteObject(bucketName, objectName string) error {
ctx := context.Background()
bucket, err := getBucketByName(bucketName)
if err != nil {
return err
}
bucketPath := bucket.Path
fp := path.Join(bucketPath, objectName)
fmeta, _ := op.GetNearestMeta(fp)
// S3 does not report an error when attemping to delete a key that does not exist, so
// we need to skip IsNotExist errors.
if _, err := fs.Get(context.WithValue(ctx, "meta", fmeta), fp, &fs.GetArgs{}); err != nil && !errs.IsObjectNotFound(err) {
return err
}
fs.Remove(ctx, fp)
return nil
}
// CreateBucket creates a new bucket.
func (b *s3Backend) CreateBucket(name string) error {
return gofakes3.ErrNotImplemented
}
// DeleteBucket deletes the bucket with the given name.
func (b *s3Backend) DeleteBucket(name string) error {
return gofakes3.ErrNotImplemented
}
// BucketExists checks if the bucket exists.
func (b *s3Backend) BucketExists(name string) (exists bool, err error) {
buckets, err := getAndParseBuckets()
if err != nil {
return false, err
}
for _, b := range buckets {
if b.Name == name {
return true, nil
}
}
return false, nil
}
// CopyObject copy specified object from srcKey to dstKey.
func (b *s3Backend) CopyObject(srcBucket, srcKey, dstBucket, dstKey string, meta map[string]string) (result gofakes3.CopyObjectResult, err error) {
if srcBucket == dstBucket && srcKey == dstKey {
//TODO: update meta
return result, nil
}
ctx := context.Background()
srcB, err := getBucketByName(srcBucket)
if err != nil {
return result, err
}
srcBucketPath := srcB.Path
srcFp := path.Join(srcBucketPath, srcKey)
fmeta, _ := op.GetNearestMeta(srcFp)
srcNode, err := fs.Get(context.WithValue(ctx, "meta", fmeta), srcFp, &fs.GetArgs{})
c, err := b.GetObject(srcBucket, srcKey, nil)
if err != nil {
return
}
defer func() {
_ = c.Contents.Close()
}()
for k, v := range c.Metadata {
if _, found := meta[k]; !found && k != "X-Amz-Acl" {
meta[k] = v
}
}
if _, ok := meta["mtime"]; !ok {
meta["mtime"] = swift.TimeToFloatString(srcNode.ModTime())
}
_, err = b.PutObject(dstBucket, dstKey, meta, c.Contents, c.Size)
if err != nil {
return
}
return gofakes3.CopyObjectResult{
ETag: `"` + hex.EncodeToString(c.Hash) + `"`,
LastModified: gofakes3.NewContentTime(srcNode.ModTime()),
}, nil
}

36
server/s3/ioutils.go Normal file
View File

@ -0,0 +1,36 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import "io"
type noOpReadCloser struct{}
type readerWithCloser struct {
io.Reader
closer func() error
}
var _ io.ReadCloser = &readerWithCloser{}
func (d noOpReadCloser) Read(b []byte) (n int, err error) {
return 0, io.EOF
}
func (d noOpReadCloser) Close() error {
return nil
}
func limitReadCloser(rdr io.Reader, closer func() error, sz int64) io.ReadCloser {
return &readerWithCloser{
Reader: io.LimitReader(rdr, sz),
closer: closer,
}
}
func (rwc *readerWithCloser) Close() error {
if rwc.closer != nil {
return rwc.closer()
}
return nil
}

53
server/s3/list.go Normal file
View File

@ -0,0 +1,53 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"path"
"strings"
"github.com/Mikubill/gofakes3"
)
func (b *s3Backend) entryListR(bucket, fdPath, name string, addPrefix bool, response *gofakes3.ObjectList) error {
fp := path.Join(bucket, fdPath)
dirEntries, err := getDirEntries(fp)
if err != nil {
return err
}
for _, entry := range dirEntries {
object := entry.GetName()
// workround for control-chars detect
objectPath := path.Join(fdPath, object)
if !strings.HasPrefix(object, name) {
continue
}
if entry.IsDir() {
if addPrefix {
// response.AddPrefix(gofakes3.URLEncode(objectPath))
response.AddPrefix(objectPath)
continue
}
err := b.entryListR(bucket, path.Join(fdPath, object), "", false, response)
if err != nil {
return err
}
} else {
item := &gofakes3.Content{
// Key: gofakes3.URLEncode(objectPath),
Key: objectPath,
LastModified: gofakes3.NewContentTime(entry.ModTime()),
ETag: getFileHash(entry),
Size: entry.GetSize(),
StorageClass: gofakes3.StorageStandard,
}
response.Add(item)
}
}
return nil
}

27
server/s3/logger.go Normal file
View File

@ -0,0 +1,27 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"fmt"
"github.com/Mikubill/gofakes3"
"github.com/alist-org/alist/v3/pkg/utils"
)
// logger output formatted message
type logger struct{}
// print log message
func (l logger) Print(level gofakes3.LogLevel, v ...interface{}) {
switch level {
default:
fallthrough
case gofakes3.LogErr:
utils.Log.Errorf("serve s3: %s", fmt.Sprintln(v...))
case gofakes3.LogWarn:
utils.Log.Infof("serve s3: %s", fmt.Sprintln(v...))
case gofakes3.LogInfo:
utils.Log.Debugf("serve s3: %s", fmt.Sprintln(v...))
}
}

67
server/s3/pager.go Normal file
View File

@ -0,0 +1,67 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"sort"
"github.com/Mikubill/gofakes3"
)
// pager splits the object list into smulitply pages.
func (db *s3Backend) pager(list *gofakes3.ObjectList, page gofakes3.ListBucketPage) (*gofakes3.ObjectList, error) {
// sort by alphabet
sort.Slice(list.CommonPrefixes, func(i, j int) bool {
return list.CommonPrefixes[i].Prefix < list.CommonPrefixes[j].Prefix
})
// sort by modtime
sort.Slice(list.Contents, func(i, j int) bool {
return list.Contents[i].LastModified.Before(list.Contents[j].LastModified.Time)
})
tokens := page.MaxKeys
if tokens == 0 {
tokens = 1000
}
if page.HasMarker {
for i, obj := range list.Contents {
if obj.Key == page.Marker {
list.Contents = list.Contents[i+1:]
break
}
}
for i, obj := range list.CommonPrefixes {
if obj.Prefix == page.Marker {
list.CommonPrefixes = list.CommonPrefixes[i+1:]
break
}
}
}
response := gofakes3.NewObjectList()
for _, obj := range list.CommonPrefixes {
if tokens <= 0 {
break
}
response.AddPrefix(obj.Prefix)
tokens--
}
for _, obj := range list.Contents {
if tokens <= 0 {
break
}
response.Add(obj)
tokens--
}
if len(list.CommonPrefixes)+len(list.Contents) > int(page.MaxKeys) {
response.IsTruncated = true
if len(response.Contents) > 0 {
response.NextMarker = response.Contents[len(response.Contents)-1].Key
} else {
response.NextMarker = response.CommonPrefixes[len(response.CommonPrefixes)-1].Prefix
}
}
return response, nil
}

27
server/s3/server.go Normal file
View File

@ -0,0 +1,27 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"context"
"math/rand"
"net/http"
"github.com/Mikubill/gofakes3"
)
// Make a new S3 Server to serve the remote
func NewServer(ctx context.Context) (h http.Handler, err error) {
var newLogger logger
faker := gofakes3.New(
newBackend(),
// gofakes3.WithHostBucket(!opt.pathBucketMode),
gofakes3.WithLogger(newLogger),
gofakes3.WithRequestID(rand.Uint64()),
gofakes3.WithoutVersioning(),
gofakes3.WithV4Auth(authlistResolver()),
gofakes3.WithIntegrityCheck(true), // Check Content-MD5 if supplied
)
return faker.Server(), nil
}

160
server/s3/utils.go Normal file
View File

@ -0,0 +1,160 @@
// Credits: https://pkg.go.dev/github.com/rclone/rclone@v1.65.2/cmd/serve/s3
// Package s3 implements a fake s3 server for alist
package s3
import (
"context"
"encoding/json"
"strings"
"github.com/Mikubill/gofakes3"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/fs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/internal/setting"
)
type Bucket struct {
Name string `json:"name"`
Path string `json:"path"`
}
func getAndParseBuckets() ([]Bucket, error) {
var res []Bucket
err := json.Unmarshal([]byte(setting.GetStr(conf.S3Buckets)), &res)
return res, err
}
func getBucketByName(name string) (Bucket, error) {
buckets, err := getAndParseBuckets()
if err != nil {
return Bucket{}, err
}
for _, b := range buckets {
if b.Name == name {
return b, nil
}
}
return Bucket{}, gofakes3.BucketNotFound(name)
}
func getDirEntries(path string) ([]model.Obj, error) {
ctx := context.Background()
meta, _ := op.GetNearestMeta(path)
fi, err := fs.Get(context.WithValue(ctx, "meta", meta), path, &fs.GetArgs{})
if errs.IsNotFoundError(err) {
return nil, gofakes3.ErrNoSuchKey
} else if err != nil {
return nil, gofakes3.ErrNoSuchKey
}
if !fi.IsDir() {
return nil, gofakes3.ErrNoSuchKey
}
dirEntries, err := fs.List(context.WithValue(ctx, "meta", meta), path, &fs.ListArgs{})
if err != nil {
return nil, err
}
return dirEntries, nil
}
// func getFileHashByte(node interface{}) []byte {
// b, err := hex.DecodeString(getFileHash(node))
// if err != nil {
// return nil
// }
// return b
// }
func getFileHash(node interface{}) string {
// var o fs.Object
// switch b := node.(type) {
// case vfs.Node:
// fsObj, ok := b.DirEntry().(fs.Object)
// if !ok {
// fs.Debugf("serve s3", "File uploading - reading hash from VFS cache")
// in, err := b.Open(os.O_RDONLY)
// if err != nil {
// return ""
// }
// defer func() {
// _ = in.Close()
// }()
// h, err := hash.NewMultiHasherTypes(hash.NewHashSet(hash.MD5))
// if err != nil {
// return ""
// }
// _, err = io.Copy(h, in)
// if err != nil {
// return ""
// }
// return h.Sums()[hash.MD5]
// }
// o = fsObj
// case fs.Object:
// o = b
// }
// hash, err := o.Hash(context.Background(), hash.MD5)
// if err != nil {
// return ""
// }
// return hash
return ""
}
func prefixParser(p *gofakes3.Prefix) (path, remaining string) {
idx := strings.LastIndexByte(p.Prefix, '/')
if idx < 0 {
return "", p.Prefix
}
return p.Prefix[:idx], p.Prefix[idx+1:]
}
// // FIXME this could be implemented by VFS.MkdirAll()
// func mkdirRecursive(path string, VFS *vfs.VFS) error {
// path = strings.Trim(path, "/")
// dirs := strings.Split(path, "/")
// dir := ""
// for _, d := range dirs {
// dir += "/" + d
// if _, err := VFS.Stat(dir); err != nil {
// err := VFS.Mkdir(dir, 0777)
// if err != nil {
// return err
// }
// }
// }
// return nil
// }
// func rmdirRecursive(p string, VFS *vfs.VFS) {
// dir := path.Dir(p)
// if !strings.ContainsAny(dir, "/\\") {
// // might be bucket(root)
// return
// }
// if _, err := VFS.Stat(dir); err == nil {
// err := VFS.Remove(dir)
// if err != nil {
// return
// }
// rmdirRecursive(dir, VFS)
// }
// }
func authlistResolver() map[string]string {
s3accesskeyid := setting.GetStr(conf.S3AccessKeyId)
s3secretaccesskey := setting.GetStr(conf.S3SecretAccessKey)
if s3accesskeyid == "" && s3secretaccesskey == "" {
return nil
}
authList := make(map[string]string)
authList[s3accesskeyid] = s3secretaccesskey
return authList
}

View File

@ -14,6 +14,7 @@ import (
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/model"
@ -385,6 +386,10 @@ func findLastModified(ctx context.Context, ls LockSystem, name string, fi model.
return fi.ModTime().UTC().Format(http.TimeFormat), nil
}
func findCreationDate(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
userAgent := ctx.Value("userAgent").(string)
if strings.Contains(strings.ToLower(userAgent), "microsoft-webdav") {
return fi.CreateTime().UTC().Format(http.TimeFormat), nil
}
return fi.CreateTime().UTC().Format(time.RFC3339), nil
}

View File

@ -8,16 +8,21 @@ import (
)
func (h *Handler) getModTime(r *http.Request) time.Time {
return h.getHeaderTime(r, "X-OC-Mtime")
return h.getHeaderTime(r, "X-OC-Mtime", "")
}
// owncloud/ nextcloud haven't impl this, but we can add the support since rclone may support this soon
// owncloud/ nextcloud haven't impl this, but we can add the support since rclone may support this soon.
// try ModTime if CreateTime not found in header
func (h *Handler) getCreateTime(r *http.Request) time.Time {
return h.getHeaderTime(r, "X-OC-Ctime")
return h.getHeaderTime(r, "X-OC-Ctime", "X-OC-Mtime")
}
func (h *Handler) getHeaderTime(r *http.Request, header string) time.Time {
func (h *Handler) getHeaderTime(r *http.Request, header, alternative string) time.Time {
hVal := r.Header.Get(header)
// try alternative
if hVal == "" && alternative != "" {
hVal = r.Header.Get(alternative)
}
if hVal != "" {
modTimeUnix, err := strconv.ParseInt(hVal, 10, 64)
if err == nil {

View File

@ -6,6 +6,7 @@
package webdav // import "golang.org/x/net/webdav"
import (
"context"
"errors"
"fmt"
"net/http"
@ -330,21 +331,21 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
Modified: h.getModTime(r),
Ctime: h.getCreateTime(r),
}
stream := &stream.FileStream{
fsStream := &stream.FileStream{
Obj: &obj,
Reader: r.Body,
Mimetype: r.Header.Get("Content-Type"),
}
if stream.Mimetype == "" {
stream.Mimetype = utils.GetMimeType(reqPath)
if fsStream.Mimetype == "" {
fsStream.Mimetype = utils.GetMimeType(reqPath)
}
err = fs.PutDirectly(ctx, path.Dir(reqPath), stream)
err = fs.PutDirectly(ctx, path.Dir(reqPath), fsStream)
if errs.IsNotFoundError(err) {
return http.StatusNotFound, err
}
_ = r.Body.Close()
_ = stream.Close()
_ = fsStream.Close()
// TODO(rost): Returning 405 Method Not Allowed might not be appropriate.
if err != nil {
return http.StatusMethodNotAllowed, err
@ -619,6 +620,8 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
return status, err
}
ctx := r.Context()
userAgent := r.Header.Get("User-Agent")
ctx = context.WithValue(ctx, "userAgent", userAgent)
user := ctx.Value("user").(*model.User)
reqPath, err = user.JoinPath(reqPath)
if err != nil {