Compare commits
6 Commits
v3.35.0
...
feat/ilanz
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af920e00d4 | ||
|
|
697512aa8d | ||
|
|
8d0f80db33 | ||
|
|
c39909c641 | ||
|
|
7868a1a524 | ||
|
|
9a356ec9d6 |
44
.air.toml
44
.air.toml
@@ -1,44 +0,0 @@
|
|||||||
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
|
|
||||||
62
.github/workflows/build_docker.yml
vendored
62
.github/workflows/build_docker.yml
vendored
@@ -3,8 +3,6 @@ name: build_docker
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
@@ -12,88 +10,42 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_docker:
|
build_docker:
|
||||||
name: Build Docker
|
name: Build docker
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: xhofe/alist
|
images: xhofe/alist
|
||||||
|
- name: Replace release with dev
|
||||||
- name: Docker meta with ffmpeg
|
run: |
|
||||||
id: meta-ffmpeg
|
sed -i 's/release/dev/g' Dockerfile
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: xhofe/alist
|
|
||||||
flavor: |
|
|
||||||
suffix=-ffmpeg,onlatest=true
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: github.event_name == 'push'
|
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: xhofe
|
username: xhofe
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: docker_build
|
id: docker_build
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile.ci
|
push: true
|
||||||
push: ${{ github.event_name == 'push' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
- 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 }}
|
|
||||||
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
|
||||||
|
|
||||||
build_docker_with_aria2:
|
build_docker_with_aria2:
|
||||||
needs: build_docker
|
needs: build_docker
|
||||||
name: Build docker with aria2
|
name: Build docker with aria2
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'push'
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -114,4 +66,4 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.MY_TOKEN }}
|
github_token: ${{ secrets.MY_TOKEN }}
|
||||||
branch: main
|
branch: main
|
||||||
repository: alist-org/with_aria2
|
repository: alist-org/with_aria2
|
||||||
34
.github/workflows/release_android.yml
vendored
34
.github/workflows/release_android.yml
vendored
@@ -1,34 +0,0 @@
|
|||||||
name: release_android
|
|
||||||
|
|
||||||
on:
|
|
||||||
release:
|
|
||||||
types: [ published ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release_android:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [ ubuntu-latest ]
|
|
||||||
go-version: [ '1.21' ]
|
|
||||||
name: Release
|
|
||||||
runs-on: ${{ matrix.platform }}
|
|
||||||
steps:
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: ${{ matrix.go-version }}
|
|
||||||
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: |
|
|
||||||
bash build.sh release android
|
|
||||||
|
|
||||||
- name: Upload assets
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
files: build/compress/*
|
|
||||||
39
.github/workflows/release_docker.yml
vendored
39
.github/workflows/release_docker.yml
vendored
@@ -13,24 +13,6 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@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
|
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -54,32 +36,11 @@ jobs:
|
|||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: Dockerfile.ci
|
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
||||||
|
|
||||||
- name: Docker meta with ffmpeg
|
|
||||||
id: meta-ffmpeg
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: xhofe/alist
|
|
||||||
flavor: |
|
|
||||||
latest=true
|
|
||||||
suffix=-ffmpeg,onlatest=true
|
|
||||||
|
|
||||||
- name: Build and push with ffmpeg
|
|
||||||
id: docker_build_ffmpeg
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.ffmpeg
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta-ffmpeg.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta-ffmpeg.outputs.labels }}
|
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/386,linux/arm/v6,linux/s390x
|
|
||||||
|
|
||||||
release_docker_with_aria2:
|
release_docker_with_aria2:
|
||||||
needs: release_docker
|
needs: release_docker
|
||||||
name: Release docker with aria2
|
name: Release docker with aria2
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,7 +24,6 @@ output/
|
|||||||
*.json
|
*.json
|
||||||
/build
|
/build
|
||||||
/data/
|
/data/
|
||||||
/tmp/
|
|
||||||
/log/
|
/log/
|
||||||
/lang/
|
/lang/
|
||||||
/daemon/
|
/daemon/
|
||||||
|
|||||||
15
Dockerfile
15
Dockerfile
@@ -1,11 +1,9 @@
|
|||||||
FROM alpine:edge as builder
|
FROM alpine:edge as builder
|
||||||
LABEL stage=go-builder
|
LABEL stage=go-builder
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
RUN apk add --no-cache bash curl gcc git go musl-dev
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
COPY ./ ./
|
COPY ./ ./
|
||||||
RUN bash build.sh release docker
|
RUN apk add --no-cache bash curl gcc git go musl-dev; \
|
||||||
|
bash build.sh release docker
|
||||||
|
|
||||||
FROM alpine:edge
|
FROM alpine:edge
|
||||||
LABEL MAINTAINER="i@nn.ci"
|
LABEL MAINTAINER="i@nn.ci"
|
||||||
@@ -13,11 +11,8 @@ VOLUME /opt/alist/data/
|
|||||||
WORKDIR /opt/alist/
|
WORKDIR /opt/alist/
|
||||||
COPY --from=builder /app/bin/alist ./
|
COPY --from=builder /app/bin/alist ./
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN apk update && \
|
RUN apk add --no-cache bash ca-certificates su-exec tzdata; \
|
||||||
apk upgrade --no-cache && \
|
chmod +x /entrypoint.sh
|
||||||
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
|
||||||
chmod +x /entrypoint.sh && \
|
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
ENV PUID=0 PGID=0 UMASK=022
|
ENV PUID=0 PGID=0 UMASK=022
|
||||||
EXPOSE 5244 5245
|
EXPOSE 5244 5245
|
||||||
CMD [ "/entrypoint.sh" ]
|
CMD [ "/entrypoint.sh" ]
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
FROM alpine:edge
|
|
||||||
ARG TARGETPLATFORM
|
|
||||||
LABEL MAINTAINER="i@nn.ci"
|
|
||||||
VOLUME /opt/alist/data/
|
|
||||||
WORKDIR /opt/alist/
|
|
||||||
COPY /build/${TARGETPLATFORM}/alist ./
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN apk update && \
|
|
||||||
apk upgrade --no-cache && \
|
|
||||||
apk add --no-cache bash ca-certificates su-exec tzdata; \
|
|
||||||
chmod +x /entrypoint.sh && \
|
|
||||||
rm -rf /var/cache/apk/* && \
|
|
||||||
/entrypoint.sh version
|
|
||||||
ENV PUID=0 PGID=0 UMASK=022
|
|
||||||
EXPOSE 5244 5245
|
|
||||||
CMD [ "/entrypoint.sh" ]
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
FROM xhofe/alist:latest
|
|
||||||
RUN apk update && \
|
|
||||||
apk add --no-cache ffmpeg \
|
|
||||||
rm -rf /var/cache/apk/*
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||||
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
|
<p><em>🗂️A file list program that supports multiple storages, powered by Gin and Solidjs.</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||||
@@ -75,8 +75,6 @@ English | [中文](./README_cn.md)| [日本語](./README_ja.md) | [Contributing]
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [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] Easy to deploy and out-of-the-box
|
||||||
- [x] File preview (PDF, markdown, code, plain text, ...)
|
- [x] File preview (PDF, markdown, code, plain text, ...)
|
||||||
- [x] Image preview in gallery mode
|
- [x] Image preview in gallery mode
|
||||||
@@ -115,7 +113,7 @@ https://alist.nn.ci/guide/sponsor.html
|
|||||||
|
|
||||||
### Special sponsors
|
### Special sponsors
|
||||||
|
|
||||||
- [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.
|
- [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.
|
||||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||||
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
|
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||||
@@ -74,8 +74,6 @@
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
- [x] [飞机盘](https://www.feijipan.com/)
|
|
||||||
- [x] [多吉云](https://www.dogecloud.com/product/oss)
|
|
||||||
- [x] 部署方便,开箱即用
|
- [x] 部署方便,开箱即用
|
||||||
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
- [x] 文件预览(PDF、markdown、代码、纯文本……)
|
||||||
- [x] 画廊模式下的图像预览
|
- [x] 画廊模式下的图像预览
|
||||||
@@ -113,7 +111,7 @@ AList 是一个开源软件,如果你碰巧喜欢这个项目,并希望我
|
|||||||
|
|
||||||
### 特别赞助
|
### 特别赞助
|
||||||
|
|
||||||
- [VidHub](https://apps.apple.com/app/apple-store/id1659622164?pt=118612019&ct=alist&mt=8) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
- [VidHub](https://zh.okaapps.com/product/1659622164?ref=alist) - 苹果生态下优雅的网盘视频播放器,iPhone,iPad,Mac,Apple TV全平台支持。
|
||||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (国内API服务器赞助)
|
||||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://alist.nn.ci"><img width="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
|
||||||
<p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
|
<p><em>🗂️Gin と Solidjs による、複数のストレージをサポートするファイルリストプログラム。</em></p>
|
||||||
<div>
|
<div>
|
||||||
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
|
||||||
@@ -75,8 +75,6 @@
|
|||||||
- [x] [115](https://115.com/)
|
- [x] [115](https://115.com/)
|
||||||
- [X] Cloudreve
|
- [X] Cloudreve
|
||||||
- [x] [Dropbox](https://www.dropbox.com/)
|
- [x] [Dropbox](https://www.dropbox.com/)
|
||||||
- [x] [FeijiPan](https://www.feijipan.com/)
|
|
||||||
- [x] [dogecloud](https://www.dogecloud.com/product/oss)
|
|
||||||
- [x] デプロイが簡単で、すぐに使える
|
- [x] デプロイが簡単で、すぐに使える
|
||||||
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
- [x] ファイルプレビュー (PDF, マークダウン, コード, プレーンテキスト, ...)
|
||||||
- [x] ギャラリーモードでの画像プレビュー
|
- [x] ギャラリーモードでの画像プレビュー
|
||||||
@@ -115,7 +113,7 @@ https://alist.nn.ci/guide/sponsor.html
|
|||||||
|
|
||||||
### スペシャルスポンサー
|
### スペシャルスポンサー
|
||||||
|
|
||||||
- [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.
|
- [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.
|
||||||
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
- [亚洲云](https://www.asiayun.com/aff/QQCOOQKZ) - 高防服务器|服务器租用|福州高防|广东电信|香港服务器|美国服务器|海外服务器 - 国内靠谱的企业级云计算服务提供商 (sponsored Chinese API server)
|
||||||
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
- [找资源](https://zhaoziyuan.pw/) - 阿里云盘资源搜索引擎
|
||||||
|
|
||||||
|
|||||||
95
build.sh
95
build.sh
@@ -85,68 +85,12 @@ BuildDev() {
|
|||||||
cat md5.txt
|
cat md5.txt
|
||||||
}
|
}
|
||||||
|
|
||||||
PrepareBuildDocker() {
|
BuildDocker() {
|
||||||
echo "replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed" >>go.mod
|
echo "replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed" >>go.mod
|
||||||
go get gorm.io/driver/sqlite@v1.4.4
|
go get gorm.io/driver/sqlite@v1.4.4
|
||||||
go mod download
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildDocker() {
|
|
||||||
PrepareBuildDocker
|
|
||||||
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
|
||||||
}
|
}
|
||||||
|
|
||||||
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"
|
|
||||||
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
|
|
||||||
|
|
||||||
OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x)
|
|
||||||
CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc)
|
|
||||||
for i in "${!OS_ARCHES[@]}"; do
|
|
||||||
os_arch=${OS_ARCHES[$i]}
|
|
||||||
cgo_cc=${CGO_ARGS[$i]}
|
|
||||||
os=${os_arch%%-*}
|
|
||||||
arch=${os_arch##*-}
|
|
||||||
export GOOS=$os
|
|
||||||
export GOARCH=$arch
|
|
||||||
export CC=${cgo_cc}
|
|
||||||
echo "building for $os_arch"
|
|
||||||
go build -o build/$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
|
||||||
done
|
|
||||||
|
|
||||||
DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
|
|
||||||
CGO_ARGS=(armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc)
|
|
||||||
GO_ARM=(6 7)
|
|
||||||
export GOOS=linux
|
|
||||||
export GOARCH=arm
|
|
||||||
for i in "${!DOCKER_ARM_ARCHES[@]}"; do
|
|
||||||
docker_arch=${DOCKER_ARM_ARCHES[$i]}
|
|
||||||
cgo_cc=${CGO_ARGS[$i]}
|
|
||||||
export GOARM=${GO_ARM[$i]}
|
|
||||||
export CC=${cgo_cc}
|
|
||||||
echo "building for $docker_arch"
|
|
||||||
go build -o build/${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildRelease() {
|
BuildRelease() {
|
||||||
rm -rf .git/
|
rm -rf .git/
|
||||||
mkdir -p "build"
|
mkdir -p "build"
|
||||||
@@ -218,27 +162,6 @@ BuildReleaseLinuxMuslArm() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildReleaseAndroid() {
|
|
||||||
rm -rf .git/
|
|
||||||
mkdir -p "build"
|
|
||||||
wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
|
|
||||||
unzip android-ndk-r26b-linux.zip
|
|
||||||
rm android-ndk-r26b-linux.zip
|
|
||||||
OS_ARCHES=(amd64 arm64 386 arm)
|
|
||||||
CGO_ARGS=(x86_64-linux-android24-clang aarch64-linux-android24-clang i686-linux-android24-clang armv7a-linux-androideabi24-clang)
|
|
||||||
for i in "${!OS_ARCHES[@]}"; do
|
|
||||||
os_arch=${OS_ARCHES[$i]}
|
|
||||||
cgo_cc=$(realpath android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/${CGO_ARGS[$i]})
|
|
||||||
echo building for android-${os_arch}
|
|
||||||
export GOOS=android
|
|
||||||
export GOARCH=${os_arch##*-}
|
|
||||||
export CC=${cgo_cc}
|
|
||||||
export CGO_ENABLED=1
|
|
||||||
go build -o ./build/$appName-android-$os_arch -ldflags="$ldflags" -tags=jsoniter .
|
|
||||||
android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip ./build/$appName-android-$os_arch
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
MakeRelease() {
|
MakeRelease() {
|
||||||
cd build
|
cd build
|
||||||
mkdir compress
|
mkdir compress
|
||||||
@@ -246,11 +169,6 @@ MakeRelease() {
|
|||||||
cp "$i" alist
|
cp "$i" alist
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
tar -czvf compress/"$i".tar.gz alist
|
||||||
rm -f alist
|
rm -f alist
|
||||||
done
|
|
||||||
for i in $(find . -type f -name "$appName-android-*"); do
|
|
||||||
cp "$i" alist
|
|
||||||
tar -czvf compress/"$i".tar.gz alist
|
|
||||||
rm -f alist
|
|
||||||
done
|
done
|
||||||
for i in $(find . -type f -name "$appName-darwin-*"); do
|
for i in $(find . -type f -name "$appName-darwin-*"); do
|
||||||
cp "$i" alist
|
cp "$i" alist
|
||||||
@@ -272,8 +190,6 @@ if [ "$1" = "dev" ]; then
|
|||||||
FetchWebDev
|
FetchWebDev
|
||||||
if [ "$2" = "docker" ]; then
|
if [ "$2" = "docker" ]; then
|
||||||
BuildDocker
|
BuildDocker
|
||||||
elif [ "$2" = "docker-multiplatform" ]; then
|
|
||||||
BuildDockerMultiplatform
|
|
||||||
else
|
else
|
||||||
BuildDev
|
BuildDev
|
||||||
fi
|
fi
|
||||||
@@ -281,25 +197,16 @@ elif [ "$1" = "release" ]; then
|
|||||||
FetchWebRelease
|
FetchWebRelease
|
||||||
if [ "$2" = "docker" ]; then
|
if [ "$2" = "docker" ]; then
|
||||||
BuildDocker
|
BuildDocker
|
||||||
elif [ "$2" = "docker-multiplatform" ]; then
|
|
||||||
BuildDockerMultiplatform
|
|
||||||
elif [ "$2" = "linux_musl_arm" ]; then
|
elif [ "$2" = "linux_musl_arm" ]; then
|
||||||
BuildReleaseLinuxMuslArm
|
BuildReleaseLinuxMuslArm
|
||||||
MakeRelease "md5-linux-musl-arm.txt"
|
MakeRelease "md5-linux-musl-arm.txt"
|
||||||
elif [ "$2" = "linux_musl" ]; then
|
elif [ "$2" = "linux_musl" ]; then
|
||||||
BuildReleaseLinuxMusl
|
BuildReleaseLinuxMusl
|
||||||
MakeRelease "md5-linux-musl.txt"
|
MakeRelease "md5-linux-musl.txt"
|
||||||
elif [ "$2" = "android" ]; then
|
|
||||||
BuildReleaseAndroid
|
|
||||||
MakeRelease "md5-android.txt"
|
|
||||||
else
|
else
|
||||||
BuildRelease
|
BuildRelease
|
||||||
MakeRelease "md5.txt"
|
MakeRelease "md5.txt"
|
||||||
fi
|
fi
|
||||||
elif [ "$1" = "prepare" ]; then
|
|
||||||
if [ "$2" = "docker-multiplatform" ]; then
|
|
||||||
PrepareBuildDockerMusl
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo -e "Parameter error"
|
echo -e "Parameter error"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -91,27 +91,6 @@ 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
|
// Wait for interrupt signal to gracefully shutdown the server with
|
||||||
// a timeout of 1 second.
|
// a timeout of 1 second.
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
||||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
||||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
|
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
|
||||||
PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
|
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
||||||
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
|
||||||
driver.RootID
|
driver.RootID
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,7 +18,7 @@ var config = driver.Config{
|
|||||||
DefaultRoot: "0",
|
DefaultRoot: "0",
|
||||||
//OnlyProxy: true,
|
//OnlyProxy: true,
|
||||||
//OnlyLocal: true,
|
//OnlyLocal: true,
|
||||||
//NoOverwriteUpload: true,
|
NoOverwriteUpload: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (d *Pan115) login() error {
|
|||||||
s := &driver115.QRCodeSession{
|
s := &driver115.QRCodeSession{
|
||||||
UID: d.Addition.QRCodeToken,
|
UID: d.Addition.QRCodeToken,
|
||||||
}
|
}
|
||||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
if cr, err = d.client.QRCodeLogin(s); err != nil {
|
||||||
return errors.Wrap(err, "failed to login by qrcode")
|
return errors.Wrap(err, "failed to login by qrcode")
|
||||||
}
|
}
|
||||||
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
|
||||||
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
|
||||||
QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
|
PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
|
||||||
PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
|
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
||||||
LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
|
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
|
||||||
ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
|
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
|
||||||
ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
|
|
||||||
driver.RootID
|
driver.RootID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func (d *Pan115Share) login() error {
|
|||||||
s := &driver115.QRCodeSession{
|
s := &driver115.QRCodeSession{
|
||||||
UID: d.QRCodeToken,
|
UID: d.QRCodeToken,
|
||||||
}
|
}
|
||||||
if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
|
if cr, err = d.client.QRCodeLogin(s); err != nil {
|
||||||
return errors.Wrap(err, "failed to login by qrcode")
|
return errors.Wrap(err, "failed to login by qrcode")
|
||||||
}
|
}
|
||||||
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/time/rate"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
@@ -24,12 +17,14 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Pan123 struct {
|
type Pan123 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
apiRateLimit sync.Map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) Config() driver.Config {
|
func (d *Pan123) Config() driver.Config {
|
||||||
@@ -194,7 +189,7 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
defer func() {
|
defer func() {
|
||||||
_ = tempFile.Close()
|
_ = tempFile.Close()
|
||||||
}()
|
}()
|
||||||
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
|
if _, err = io.Copy(h, tempFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
_, err = tempFile.Seek(0, io.SeekStart)
|
||||||
@@ -237,9 +232,6 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uploader := s3manager.NewUploader(s)
|
uploader := s3manager.NewUploader(s)
|
||||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
|
||||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
|
||||||
}
|
|
||||||
input := &s3manager.UploadInput{
|
input := &s3manager.UploadInput{
|
||||||
Bucket: &resp.Data.Bucket,
|
Bucket: &resp.Data.Bucket,
|
||||||
Key: &resp.Data.Key,
|
Key: &resp.Data.Key,
|
||||||
@@ -258,11 +250,4 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123) 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 = (*Pan123)(nil)
|
var _ driver.Driver = (*Pan123)(nil)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ func (d *Pan123) login() error {
|
|||||||
SetHeaders(map[string]string{
|
SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"user-agent": "Dart/2.19(dart:io)-alist",
|
"user-agent": "Dart/2.19(dart:io)",
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"app-version": "3",
|
"app-version": "3",
|
||||||
//"user-agent": base.UserAgent,
|
//"user-agent": base.UserAgent,
|
||||||
@@ -197,7 +197,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
|||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
"authorization": "Bearer " + d.AccessToken,
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0",
|
||||||
"platform": "web",
|
"platform": "web",
|
||||||
"app-version": "3",
|
"app-version": "3",
|
||||||
//"user-agent": base.UserAgent,
|
//"user-agent": base.UserAgent,
|
||||||
@@ -235,12 +235,7 @@ func (d *Pan123) request(url string, method string, callback base.ReqCallback, r
|
|||||||
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
func (d *Pan123) getFiles(parentId string) ([]File, error) {
|
||||||
page := 1
|
page := 1
|
||||||
res := make([]File, 0)
|
res := make([]File, 0)
|
||||||
// 2024-02-06 fix concurrency by 123pan
|
|
||||||
for {
|
for {
|
||||||
if !d.APIRateLimit(FileList) {
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var resp Files
|
var resp Files
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"driveId": "0",
|
"driveId": "0",
|
||||||
|
|||||||
@@ -4,11 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"golang.org/x/time/rate"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
@@ -22,7 +19,6 @@ import (
|
|||||||
type Pan123Share struct {
|
type Pan123Share struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
apiRateLimit sync.Map
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Pan123Share) Config() driver.Config {
|
func (d *Pan123Share) Config() driver.Config {
|
||||||
@@ -150,11 +146,4 @@ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
|
|||||||
// return nil, errs.NotSupport
|
// 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)
|
var _ driver.Driver = (*Pan123Share)(nil)
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
ShareKey string `json:"sharekey" required:"true"`
|
ShareKey string `json:"sharekey" required:"true"`
|
||||||
SharePwd string `json:"sharepassword"`
|
SharePwd string `json:"sharepassword" required:"true"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
AccessToken string `json:"accesstoken" type:"text"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -2,15 +2,8 @@ package _123Share
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"hash/crc32"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
@@ -22,45 +15,20 @@ const (
|
|||||||
Api = "https://www.123pan.com/api"
|
Api = "https://www.123pan.com/api"
|
||||||
AApi = "https://www.123pan.com/a/api"
|
AApi = "https://www.123pan.com/a/api"
|
||||||
BApi = "https://www.123pan.com/b/api"
|
BApi = "https://www.123pan.com/b/api"
|
||||||
MainApi = BApi
|
MainApi = Api
|
||||||
FileList = MainApi + "/share/get"
|
FileList = MainApi + "/share/get"
|
||||||
DownloadInfo = MainApi + "/share/download/info"
|
DownloadInfo = MainApi + "/share/download/info"
|
||||||
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
|
//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) {
|
func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"origin": "https://www.123pan.com",
|
"origin": "https://www.123pan.com",
|
||||||
"referer": "https://www.123pan.com/",
|
"referer": "https://www.123pan.com/",
|
||||||
"authorization": "Bearer " + d.AccessToken,
|
"user-agent": "Dart/2.19(dart:io)",
|
||||||
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
|
"platform": "android",
|
||||||
"platform": "web",
|
"app-version": "36",
|
||||||
"app-version": "3",
|
|
||||||
//"user-agent": base.UserAgent,
|
|
||||||
})
|
})
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
@@ -68,7 +36,7 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
|
|||||||
if resp != nil {
|
if resp != nil {
|
||||||
req.SetResult(resp)
|
req.SetResult(resp)
|
||||||
}
|
}
|
||||||
res, err := req.Execute(method, GetApi(url))
|
res, err := req.Execute(method, url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -84,10 +52,6 @@ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
|
|||||||
page := 1
|
page := 1
|
||||||
res := make([]File, 0)
|
res := make([]File, 0)
|
||||||
for {
|
for {
|
||||||
if !d.APIRateLimit(FileList) {
|
|
||||||
time.Sleep(time.Millisecond * 200)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var resp Files
|
var resp Files
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"limit": "100",
|
"limit": "100",
|
||||||
|
|||||||
@@ -8,21 +8,18 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/pkg/cron"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Yun139 struct {
|
type Yun139 struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
cron *cron.Cron
|
|
||||||
Account string
|
Account string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,13 +35,6 @@ func (d *Yun139) Init(ctx context.Context) error {
|
|||||||
if d.Authorization == "" {
|
if d.Authorization == "" {
|
||||||
return fmt.Errorf("authorization is empty")
|
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 {
|
switch d.Addition.Type {
|
||||||
case MetaPersonalNew:
|
case MetaPersonalNew:
|
||||||
if len(d.Addition.RootFolderID) == 0 {
|
if len(d.Addition.RootFolderID) == 0 {
|
||||||
@@ -82,9 +72,6 @@ func (d *Yun139) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Yun139) Drop(ctx context.Context) error {
|
func (d *Yun139) Drop(ctx context.Context) error {
|
||||||
if d.cron != nil {
|
|
||||||
d.cron.Stop()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,12 @@ type Addition struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "139Yun",
|
Name: "139Yun",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
ProxyRangeOption: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
d := &Yun139{}
|
return &Yun139{}
|
||||||
d.ProxyRange = true
|
|
||||||
return d
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
package _139
|
package _139
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MetaPersonal string = "personal"
|
MetaPersonal string = "personal"
|
||||||
MetaFamily string = "family"
|
MetaFamily string = "family"
|
||||||
@@ -234,12 +230,3 @@ type PersonalUploadResp struct {
|
|||||||
UploadId string `json:"uploadId"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
"github.com/alist-org/alist/v3/pkg/utils/random"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
jsoniter "github.com/json-iterator/go"
|
jsoniter "github.com/json-iterator/go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -53,32 +52,6 @@ func getTime(t string) time.Time {
|
|||||||
return stamp
|
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) {
|
func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
url := "https://yun.139.com" + pathname
|
url := "https://yun.139.com" + pathname
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package _189pc
|
package _189pc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/ring"
|
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -29,9 +28,6 @@ type Cloud189PC struct {
|
|||||||
|
|
||||||
uploadThread int
|
uploadThread int
|
||||||
|
|
||||||
familyTransferFolder *ring.Ring
|
|
||||||
cleanFamilyTransferFile func()
|
|
||||||
|
|
||||||
storageConfig driver.Config
|
storageConfig driver.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +52,7 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
if !y.isFamily() && y.RootFolderID == "" {
|
if !y.isFamily() && y.RootFolderID == "" {
|
||||||
y.RootFolderID = "-11"
|
y.RootFolderID = "-11"
|
||||||
|
y.FamilyID = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制上传线程数
|
// 限制上传线程数
|
||||||
@@ -82,24 +79,11 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理家庭云ID
|
// 处理家庭云ID
|
||||||
if y.FamilyID == "" {
|
if y.isFamily() && y.FamilyID == "" {
|
||||||
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
if y.FamilyID, err = y.getFamilyID(); err != nil {
|
||||||
return err
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +92,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) {
|
func (y *Cloud189PC) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
return y.getFiles(ctx, dir.GetID(), y.isFamily())
|
return y.getFiles(ctx, dir.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
@@ -116,9 +100,8 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
URL string `json:"fileDownloadUrl"`
|
URL string `json:"fileDownloadUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
isFamily := y.isFamily()
|
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
}
|
}
|
||||||
fullUrl += "/getFileDownloadUrl.action"
|
fullUrl += "/getFileDownloadUrl.action"
|
||||||
@@ -126,7 +109,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
_, err := y.get(fullUrl, func(r *resty.Request) {
|
_, err := y.get(fullUrl, func(r *resty.Request) {
|
||||||
r.SetContext(ctx)
|
r.SetContext(ctx)
|
||||||
r.SetQueryParam("fileId", file.GetID())
|
r.SetQueryParam("fileId", file.GetID())
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
r.SetQueryParams(map[string]string{
|
r.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
})
|
})
|
||||||
@@ -136,7 +119,7 @@ func (y *Cloud189PC) Link(ctx context.Context, file model.Obj, args model.LinkAr
|
|||||||
"flag": "1",
|
"flag": "1",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &downloadUrl, isFamily)
|
}, &downloadUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -173,9 +156,8 @@ 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) {
|
func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
||||||
isFamily := y.isFamily()
|
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
}
|
}
|
||||||
fullUrl += "/createFolder.action"
|
fullUrl += "/createFolder.action"
|
||||||
@@ -187,7 +169,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
|||||||
"folderName": dirName,
|
"folderName": dirName,
|
||||||
"relativePath": "",
|
"relativePath": "",
|
||||||
})
|
})
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
req.SetQueryParams(map[string]string{
|
req.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
"parentId": parentDir.GetID(),
|
"parentId": parentDir.GetID(),
|
||||||
@@ -197,7 +179,7 @@ func (y *Cloud189PC) MakeDir(ctx context.Context, parentDir model.Obj, dirName s
|
|||||||
"parentFolderId": parentDir.GetID(),
|
"parentFolderId": parentDir.GetID(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &newFolder, isFamily)
|
}, &newFolder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -205,14 +187,27 @@ 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) {
|
func (y *Cloud189PC) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
||||||
isFamily := y.isFamily()
|
var resp CreateBatchTaskResp
|
||||||
other := map[string]string{"targetFileName": dstDir.GetName()}
|
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
resp, err := y.CreateBatchTask("MOVE", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
req.SetFormData(map[string]string{
|
||||||
FileId: srcObj.GetID(),
|
"type": "MOVE",
|
||||||
FileName: srcObj.GetName(),
|
"taskInfos": MustString(utils.Json.MarshalToString(
|
||||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
[]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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -223,11 +218,10 @@ 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) {
|
func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
||||||
isFamily := y.isFamily()
|
|
||||||
queryParam := make(map[string]string)
|
queryParam := make(map[string]string)
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
method := http.MethodPost
|
method := http.MethodPost
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
method = http.MethodGet
|
method = http.MethodGet
|
||||||
queryParam["familyId"] = y.FamilyID
|
queryParam["familyId"] = y.FamilyID
|
||||||
@@ -251,7 +245,7 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
|||||||
|
|
||||||
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
_, err := y.request(fullUrl, method, func(req *resty.Request) {
|
||||||
req.SetContext(ctx).SetQueryParams(queryParam)
|
req.SetContext(ctx).SetQueryParams(queryParam)
|
||||||
}, nil, newObj, isFamily)
|
}, nil, newObj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -259,15 +253,28 @@ func (y *Cloud189PC) Rename(ctx context.Context, srcObj model.Obj, newName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
isFamily := y.isFamily()
|
var resp CreateBatchTaskResp
|
||||||
other := map[string]string{"targetFileName": dstDir.GetName()}
|
_, err := y.post(API_URL+"/batch/createBatchTask.action", func(req *resty.Request) {
|
||||||
|
req.SetContext(ctx)
|
||||||
resp, err := y.CreateBatchTask("COPY", IF(isFamily, y.FamilyID, ""), dstDir.GetID(), other, BatchTaskInfo{
|
req.SetFormData(map[string]string{
|
||||||
FileId: srcObj.GetID(),
|
"type": "COPY",
|
||||||
FileName: srcObj.GetName(),
|
"taskInfos": MustString(utils.Json.MarshalToString(
|
||||||
IsFolder: BoolToNumber(srcObj.IsDir()),
|
[]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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -275,13 +282,27 @@ func (y *Cloud189PC) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
isFamily := y.isFamily()
|
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()),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
|
||||||
resp, err := y.CreateBatchTask("DELETE", IF(isFamily, y.FamilyID, ""), "", nil, BatchTaskInfo{
|
if y.isFamily() {
|
||||||
FileId: obj.GetID(),
|
req.SetFormData(map[string]string{
|
||||||
FileName: obj.GetName(),
|
"familyId": y.FamilyID,
|
||||||
IsFolder: BoolToNumber(obj.IsDir()),
|
})
|
||||||
})
|
}
|
||||||
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -289,73 +310,25 @@ func (y *Cloud189PC) Remove(ctx context.Context, obj model.Obj) error {
|
|||||||
return y.WaitBatchTask("DELETE", resp.TaskID, time.Millisecond*200)
|
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) (newObj model.Obj, err error) {
|
func (y *Cloud189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
overwrite := true
|
|
||||||
isFamily := y.isFamily()
|
|
||||||
|
|
||||||
// 响应时间长,按需启用
|
// 响应时间长,按需启用
|
||||||
if y.Addition.RapidUpload && !stream.IsForceStreamUpload() {
|
if y.Addition.RapidUpload {
|
||||||
if newObj, err := y.RapidUpload(ctx, dstDir, stream, isFamily, overwrite); err == nil {
|
if newObj, err := y.RapidUpload(ctx, dstDir, stream); err == nil {
|
||||||
return newObj, nil
|
return newObj, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadMethod := y.UploadMethod
|
switch y.UploadMethod {
|
||||||
if stream.IsForceStreamUpload() {
|
case "old":
|
||||||
uploadMethod = "stream"
|
return y.OldUpload(ctx, dstDir, stream, up)
|
||||||
}
|
|
||||||
|
|
||||||
// 旧版上传家庭云也有限制
|
|
||||||
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":
|
case "rapid":
|
||||||
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
return y.FastUpload(ctx, dstDir, stream, up)
|
||||||
case "stream":
|
case "stream":
|
||||||
if stream.GetSize() == 0 {
|
if stream.GetSize() == 0 {
|
||||||
return y.FastUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
return y.FastUpload(ctx, dstDir, stream, up)
|
||||||
}
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
default:
|
default:
|
||||||
return y.StreamUpload(ctx, dstDir, stream, up, isFamily, overwrite)
|
return y.StreamUpload(ctx, dstDir, stream, up)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,19 +192,3 @@ func partSize(size int64) int64 {
|
|||||||
}
|
}
|
||||||
return DEFAULT
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ type Addition struct {
|
|||||||
FamilyID string `json:"family_id"`
|
FamilyID string `json:"family_id"`
|
||||||
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
|
UploadMethod string `json:"upload_method" type:"select" options:"stream,rapid,old" default:"stream"`
|
||||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
FamilyTransfer bool `json:"family_transfer"`
|
|
||||||
RapidUpload bool `json:"rapid_upload"`
|
RapidUpload bool `json:"rapid_upload"`
|
||||||
NoUseOcr bool `json:"no_use_ocr"`
|
NoUseOcr bool `json:"no_use_ocr"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ package _189pc
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 居然有四种返回方式
|
// 居然有四种返回方式
|
||||||
@@ -143,7 +142,7 @@ type FamilyInfoListResp struct {
|
|||||||
type FamilyInfoResp struct {
|
type FamilyInfoResp struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
CreateTime string `json:"createTime"`
|
CreateTime string `json:"createTime"`
|
||||||
FamilyID int64 `json:"familyId"`
|
FamilyID int `json:"familyId"`
|
||||||
RemarkName string `json:"remarkName"`
|
RemarkName string `json:"remarkName"`
|
||||||
Type int `json:"type"`
|
Type int `json:"type"`
|
||||||
UseFlag int `json:"useFlag"`
|
UseFlag int `json:"useFlag"`
|
||||||
@@ -243,12 +242,7 @@ type BatchTaskInfo struct {
|
|||||||
// IsFolder 是否是文件夹,0-否,1-是
|
// IsFolder 是否是文件夹,0-否,1-是
|
||||||
IsFolder int `json:"isFolder"`
|
IsFolder int `json:"isFolder"`
|
||||||
// SrcParentId 文件所在父目录ID
|
// SrcParentId 文件所在父目录ID
|
||||||
SrcParentId string `json:"srcParentId,omitempty"`
|
//SrcParentId string `json:"srcParentId"`
|
||||||
|
|
||||||
/* 冲突管理 */
|
|
||||||
// 1 -> 跳过 2 -> 保留 3 -> 覆盖
|
|
||||||
DealWay int `json:"dealWay,omitempty"`
|
|
||||||
IsConflict int `json:"isConflict,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 上传部分 */
|
/* 上传部分 */
|
||||||
@@ -361,14 +355,6 @@ type BatchTaskStateResp struct {
|
|||||||
TaskStatus int `json:"taskStatus"` //1 初始化 2 存在冲突 3 执行中,4 完成
|
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 加密参数*/
|
/* query 加密参数*/
|
||||||
type Params map[string]string
|
type Params map[string]string
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package _189pc
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"container/ring"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@@ -55,11 +54,11 @@ const (
|
|||||||
CHANNEL_ID = "web_cloud.189.cn"
|
CHANNEL_ID = "web_cloud.189.cn"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
func (y *Cloud189PC) SignatureHeader(url, method, params string) map[string]string {
|
||||||
dateOfGmt := getHttpDateStr()
|
dateOfGmt := getHttpDateStr()
|
||||||
sessionKey := y.tokenInfo.SessionKey
|
sessionKey := y.tokenInfo.SessionKey
|
||||||
sessionSecret := y.tokenInfo.SessionSecret
|
sessionSecret := y.tokenInfo.SessionSecret
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
sessionKey = y.tokenInfo.FamilySessionKey
|
sessionKey = y.tokenInfo.FamilySessionKey
|
||||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
sessionSecret = y.tokenInfo.FamilySessionSecret
|
||||||
}
|
}
|
||||||
@@ -73,9 +72,9 @@ func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool)
|
|||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
|
func (y *Cloud189PC) EncryptParams(params Params) string {
|
||||||
sessionSecret := y.tokenInfo.SessionSecret
|
sessionSecret := y.tokenInfo.SessionSecret
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
sessionSecret = y.tokenInfo.FamilySessionSecret
|
sessionSecret = y.tokenInfo.FamilySessionSecret
|
||||||
}
|
}
|
||||||
if params != nil {
|
if params != nil {
|
||||||
@@ -84,17 +83,17 @@ func (y *Cloud189PC) EncryptParams(params Params, isFamily bool) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, params Params, resp interface{}) ([]byte, error) {
|
||||||
req := y.client.R().SetQueryParams(clientSuffix())
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
// 设置params
|
// 设置params
|
||||||
paramsData := y.EncryptParams(params, isBool(isFamily...))
|
paramsData := y.EncryptParams(params)
|
||||||
if paramsData != "" {
|
if paramsData != "" {
|
||||||
req.SetQueryParam("params", paramsData)
|
req.SetQueryParam("params", paramsData)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature
|
// Signature
|
||||||
req.SetHeaders(y.SignatureHeader(url, method, paramsData, isBool(isFamily...)))
|
req.SetHeaders(y.SignatureHeader(url, method, paramsData))
|
||||||
|
|
||||||
var erron RespErr
|
var erron RespErr
|
||||||
req.SetError(&erron)
|
req.SetError(&erron)
|
||||||
@@ -130,15 +129,15 @@ func (y *Cloud189PC) request(url, method string, callback base.ReqCallback, para
|
|||||||
return res.Body(), nil
|
return res.Body(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189PC) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
return y.request(url, http.MethodGet, callback, nil, resp, isFamily...)
|
return y.request(url, http.MethodGet, callback, nil, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189PC) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
return y.request(url, http.MethodPost, callback, nil, resp, isFamily...)
|
return y.request(url, http.MethodPost, callback, nil, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader, isFamily bool) ([]byte, error) {
|
func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]string, sign bool, file io.Reader) ([]byte, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -155,7 +154,7 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
|
|||||||
}
|
}
|
||||||
|
|
||||||
if sign {
|
if sign {
|
||||||
for key, value := range y.SignatureHeader(url, http.MethodPut, "", isFamily) {
|
for key, value := range y.SignatureHeader(url, http.MethodPut, "") {
|
||||||
req.Header.Add(key, value)
|
req.Header.Add(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,9 +181,9 @@ func (y *Cloud189PC) put(ctx context.Context, url string, headers map[string]str
|
|||||||
}
|
}
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool) ([]model.Obj, error) {
|
func (y *Cloud189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, error) {
|
||||||
fullUrl := API_URL
|
fullUrl := API_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family/file"
|
fullUrl += "/family/file"
|
||||||
}
|
}
|
||||||
fullUrl += "/listFiles.action"
|
fullUrl += "/listFiles.action"
|
||||||
@@ -202,7 +201,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool)
|
|||||||
"pageNum": fmt.Sprint(pageNum),
|
"pageNum": fmt.Sprint(pageNum),
|
||||||
"pageSize": "130",
|
"pageSize": "130",
|
||||||
})
|
})
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
r.SetQueryParams(map[string]string{
|
r.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
"orderBy": toFamilyOrderBy(y.OrderBy),
|
"orderBy": toFamilyOrderBy(y.OrderBy),
|
||||||
@@ -215,7 +214,7 @@ func (y *Cloud189PC) getFiles(ctx context.Context, fileId string, isFamily bool)
|
|||||||
"descending": toDesc(y.OrderDirection),
|
"descending": toDesc(y.OrderDirection),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &resp, isFamily)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -438,7 +437,7 @@ func (y *Cloud189PC) refreshSession() (err error) {
|
|||||||
|
|
||||||
// 普通上传
|
// 普通上传
|
||||||
// 无法上传大小为0的文件
|
// 无法上传大小为0的文件
|
||||||
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
var sliceSize = partSize(file.GetSize())
|
var sliceSize = partSize(file.GetSize())
|
||||||
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
|
count := int(math.Ceil(float64(file.GetSize()) / float64(sliceSize)))
|
||||||
lastPartSize := file.GetSize() % sliceSize
|
lastPartSize := file.GetSize() % sliceSize
|
||||||
@@ -455,7 +454,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullUrl := UPLOAD_URL
|
fullUrl := UPLOAD_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
params.Set("familyId", y.FamilyID)
|
params.Set("familyId", y.FamilyID)
|
||||||
fullUrl += "/family"
|
fullUrl += "/family"
|
||||||
} else {
|
} else {
|
||||||
@@ -467,7 +466,7 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
var initMultiUpload InitMultiUploadResp
|
var initMultiUpload InitMultiUploadResp
|
||||||
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
_, err := y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
}, params, &initMultiUpload, isFamily)
|
}, params, &initMultiUpload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -503,14 +502,14 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
|
partInfo := fmt.Sprintf("%d-%s", i, base64.StdEncoding.EncodeToString(md5Bytes))
|
||||||
|
|
||||||
threadG.Go(func(ctx context.Context) error {
|
threadG.Go(func(ctx context.Context) error {
|
||||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, initMultiUpload.Data.UploadFileID, partInfo)
|
uploadUrls, err := y.GetMultiUploadUrls(ctx, initMultiUpload.Data.UploadFileID, partInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// step.4 上传切片
|
// step.4 上传切片
|
||||||
uploadUrl := uploadUrls[0]
|
uploadUrl := uploadUrls[0]
|
||||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData), isFamily)
|
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, bytes.NewReader(byteData))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -539,21 +538,21 @@ func (y *Cloud189PC) StreamUpload(ctx context.Context, dstDir model.Obj, file mo
|
|||||||
"sliceMd5": sliceMd5Hex,
|
"sliceMd5": sliceMd5Hex,
|
||||||
"lazyCheck": "1",
|
"lazyCheck": "1",
|
||||||
"isLog": "0",
|
"isLog": "0",
|
||||||
"opertype": IF(overwrite, "3", "1"),
|
"opertype": "3",
|
||||||
}, &resp, isFamily)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return resp.toFile(), nil
|
return resp.toFile(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) (model.Obj, error) {
|
||||||
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||||
if len(fileMd5) < utils.MD5.Width {
|
if len(fileMd5) < utils.MD5.Width {
|
||||||
return nil, errors.New("invalid hash")
|
return nil, errors.New("invalid hash")
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()), isFamily)
|
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, stream.GetName(), fmt.Sprint(stream.GetSize()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -562,11 +561,11 @@ func (y *Cloud189PC) RapidUpload(ctx context.Context, dstDir model.Obj, stream m
|
|||||||
return nil, errors.New("rapid upload fail")
|
return nil, errors.New("rapid upload fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId, isFamily, overwrite)
|
return y.OldUploadCommit(ctx, uploadInfo.FileCommitUrl, uploadInfo.UploadFileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 快传
|
// 快传
|
||||||
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
tempFile, err := file.CacheFullInTempFile()
|
tempFile, err := file.CacheFullInTempFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -595,7 +594,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
silceMd5.Reset()
|
silceMd5.Reset()
|
||||||
if _, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
|
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, byteSize); err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
md5Byte := silceMd5.Sum(nil)
|
md5Byte := silceMd5.Sum(nil)
|
||||||
@@ -610,7 +609,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
fullUrl := UPLOAD_URL
|
fullUrl := UPLOAD_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family"
|
fullUrl += "/family"
|
||||||
} else {
|
} else {
|
||||||
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
//params.Set("extend", `{"opScene":"1","relativepath":"","rootfolderid":""}`)
|
||||||
@@ -629,13 +628,13 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
"sliceSize": fmt.Sprint(sliceSize),
|
"sliceSize": fmt.Sprint(sliceSize),
|
||||||
"sliceMd5": sliceMd5Hex,
|
"sliceMd5": sliceMd5Hex,
|
||||||
}
|
}
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
params.Set("familyId", y.FamilyID)
|
params.Set("familyId", y.FamilyID)
|
||||||
}
|
}
|
||||||
var uploadInfo InitMultiUploadResp
|
var uploadInfo InitMultiUploadResp
|
||||||
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
}, params, &uploadInfo, isFamily)
|
}, params, &uploadInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -660,7 +659,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
i, uploadPart := i, uploadPart
|
i, uploadPart := i, uploadPart
|
||||||
threadG.Go(func(ctx context.Context) error {
|
threadG.Go(func(ctx context.Context) error {
|
||||||
// step.3 获取上传链接
|
// step.3 获取上传链接
|
||||||
uploadUrls, err := y.GetMultiUploadUrls(ctx, isFamily, uploadInfo.UploadFileID, uploadPart)
|
uploadUrls, err := y.GetMultiUploadUrls(ctx, uploadInfo.UploadFileID, uploadPart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -672,7 +671,7 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
}
|
}
|
||||||
|
|
||||||
// step.4 上传切片
|
// step.4 上传切片
|
||||||
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize), isFamily)
|
_, err = y.put(ctx, uploadUrl.RequestURL, uploadUrl.Headers, false, io.NewSectionReader(tempFile, offset, byteSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -699,8 +698,8 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
}, Params{
|
}, Params{
|
||||||
"uploadFileId": uploadInfo.UploadFileID,
|
"uploadFileId": uploadInfo.UploadFileID,
|
||||||
"isLog": "0",
|
"isLog": "0",
|
||||||
"opertype": IF(overwrite, "3", "1"),
|
"opertype": "3",
|
||||||
}, &resp, isFamily)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -709,9 +708,9 @@ func (y *Cloud189PC) FastUpload(ctx context.Context, dstDir model.Obj, file mode
|
|||||||
|
|
||||||
// 获取上传切片信息
|
// 获取上传切片信息
|
||||||
// 对http body有大小限制,分片信息太多会出错
|
// 对http body有大小限制,分片信息太多会出错
|
||||||
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
|
func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, uploadFileId string, partInfo ...string) ([]UploadUrlInfo, error) {
|
||||||
fullUrl := UPLOAD_URL
|
fullUrl := UPLOAD_URL
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl += "/family"
|
fullUrl += "/family"
|
||||||
} else {
|
} else {
|
||||||
fullUrl += "/person"
|
fullUrl += "/person"
|
||||||
@@ -724,7 +723,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uplo
|
|||||||
}, Params{
|
}, Params{
|
||||||
"uploadFileId": uploadFileId,
|
"uploadFileId": uploadFileId,
|
||||||
"partInfo": strings.Join(partInfo, ","),
|
"partInfo": strings.Join(partInfo, ","),
|
||||||
}, &uploadUrlsResp, isFamily)
|
}, &uploadUrlsResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -753,7 +752,7 @@ func (y *Cloud189PC) GetMultiUploadUrls(ctx context.Context, isFamily bool, uplo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 旧版本上传,家庭云不支持覆盖
|
// 旧版本上传,家庭云不支持覆盖
|
||||||
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
tempFile, err := file.CacheFullInTempFile()
|
tempFile, err := file.CacheFullInTempFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -764,7 +763,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()), isFamily)
|
uploadInfo, err := y.OldUploadCreate(ctx, dstDir.GetID(), fileMd5, file.GetName(), fmt.Sprint(file.GetSize()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -781,14 +780,14 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
|||||||
"Expect": "100-continue",
|
"Expect": "100-continue",
|
||||||
}
|
}
|
||||||
|
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
header["FamilyId"] = fmt.Sprint(y.FamilyID)
|
header["FamilyId"] = fmt.Sprint(y.FamilyID)
|
||||||
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
header["UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||||
} else {
|
} else {
|
||||||
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
header["Edrive-UploadFileId"] = fmt.Sprint(status.UploadFileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile), isFamily)
|
_, err := y.put(ctx, status.FileUploadUrl, header, true, io.NopCloser(tempFile))
|
||||||
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
if err, ok := err.(*RespErr); ok && err.Code != "InputStreamReadError" {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -803,10 +802,10 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
|||||||
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
"uploadFileId": fmt.Sprint(status.UploadFileId),
|
||||||
"resumePolicy": "1",
|
"resumePolicy": "1",
|
||||||
})
|
})
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
|
req.SetQueryParam("familyId", fmt.Sprint(y.FamilyID))
|
||||||
}
|
}
|
||||||
}, &status, isFamily)
|
}, &status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -816,20 +815,20 @@ func (y *Cloud189PC) OldUpload(ctx context.Context, dstDir model.Obj, file model
|
|||||||
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
|
up(float64(status.GetSize()) / float64(file.GetSize()) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId, isFamily, overwrite)
|
return y.OldUploadCommit(ctx, status.FileCommitUrl, status.UploadFileId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建上传会话
|
// 创建上传会话
|
||||||
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string, isFamily bool) (*CreateUploadFileResp, error) {
|
func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileMd5, fileName, fileSize string) (*CreateUploadFileResp, error) {
|
||||||
var uploadInfo CreateUploadFileResp
|
var uploadInfo CreateUploadFileResp
|
||||||
|
|
||||||
fullUrl := API_URL + "/createUploadFile.action"
|
fullUrl := API_URL + "/createUploadFile.action"
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
fullUrl = API_URL + "/family/file/createFamilyFile.action"
|
fullUrl = API_URL + "/family/file/createFamilyFile.action"
|
||||||
}
|
}
|
||||||
_, err := y.post(fullUrl, func(req *resty.Request) {
|
_, err := y.post(fullUrl, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
req.SetQueryParams(map[string]string{
|
req.SetQueryParams(map[string]string{
|
||||||
"familyId": y.FamilyID,
|
"familyId": y.FamilyID,
|
||||||
"parentId": parentID,
|
"parentId": parentID,
|
||||||
@@ -850,7 +849,7 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
|
|||||||
"isLog": "0",
|
"isLog": "0",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &uploadInfo, isFamily)
|
}, &uploadInfo)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -859,11 +858,11 @@ func (y *Cloud189PC) OldUploadCreate(ctx context.Context, parentID string, fileM
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 提交上传文件
|
// 提交上传文件
|
||||||
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string, uploadFileID int64) (model.Obj, error) {
|
||||||
var resp OldCommitUploadFileResp
|
var resp OldCommitUploadFileResp
|
||||||
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
|
_, err := y.post(fileCommitUrl, func(req *resty.Request) {
|
||||||
req.SetContext(ctx)
|
req.SetContext(ctx)
|
||||||
if isFamily {
|
if y.isFamily() {
|
||||||
req.SetHeaders(map[string]string{
|
req.SetHeaders(map[string]string{
|
||||||
"ResumePolicy": "1",
|
"ResumePolicy": "1",
|
||||||
"UploadFileId": fmt.Sprint(uploadFileID),
|
"UploadFileId": fmt.Sprint(uploadFileID),
|
||||||
@@ -871,13 +870,13 @@ func (y *Cloud189PC) OldUploadCommit(ctx context.Context, fileCommitUrl string,
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
req.SetFormData(map[string]string{
|
req.SetFormData(map[string]string{
|
||||||
"opertype": IF(overwrite, "3", "1"),
|
"opertype": "3",
|
||||||
"resumePolicy": "1",
|
"resumePolicy": "1",
|
||||||
"uploadFileId": fmt.Sprint(uploadFileID),
|
"uploadFileId": fmt.Sprint(uploadFileID),
|
||||||
"isLog": "0",
|
"isLog": "0",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, &resp, isFamily)
|
}, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -896,100 +895,10 @@ func (y *Cloud189PC) isLogin() bool {
|
|||||||
return err == nil
|
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) {
|
func (y *Cloud189PC) getFamilyInfoList() ([]FamilyInfoResp, error) {
|
||||||
var resp FamilyInfoListResp
|
var resp FamilyInfoListResp
|
||||||
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp, true)
|
_, err := y.get(API_URL+"/family/manage/getFamilyList.action", nil, &resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -1013,73 +922,6 @@ func (y *Cloud189PC) getFamilyID() (string, error) {
|
|||||||
return fmt.Sprint(infos[0].FamilyID), nil
|
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) {
|
func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStateResp, error) {
|
||||||
var resp BatchTaskStateResp
|
var resp BatchTaskStateResp
|
||||||
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
|
_, err := y.post(API_URL+"/batch/checkBatchTask.action", func(req *resty.Request) {
|
||||||
@@ -1094,37 +936,6 @@ func (y *Cloud189PC) CheckBatchTask(aType string, taskID string) (*BatchTaskStat
|
|||||||
return &resp, nil
|
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 {
|
func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration) error {
|
||||||
for {
|
for {
|
||||||
state, err := y.CheckBatchTask(aType, taskID)
|
state, err := y.CheckBatchTask(aType, taskID)
|
||||||
@@ -1133,7 +944,7 @@ func (y *Cloud189PC) WaitBatchTask(aType string, taskID string, t time.Duration)
|
|||||||
}
|
}
|
||||||
switch state.TaskStatus {
|
switch state.TaskStatus {
|
||||||
case 2:
|
case 2:
|
||||||
return ErrIsConflict
|
return errors.New("there is a conflict with the target object")
|
||||||
case 4:
|
case 4:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
"github.com/alist-org/alist/v3/internal/errs"
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -46,9 +45,6 @@ func (d *Alias) Init(ctx context.Context) error {
|
|||||||
d.oneKey = k
|
d.oneKey = k
|
||||||
}
|
}
|
||||||
d.autoFlatten = true
|
d.autoFlatten = true
|
||||||
} else {
|
|
||||||
d.oneKey = ""
|
|
||||||
d.autoFlatten = false
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -115,26 +111,4 @@ func (d *Alias) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (
|
|||||||
return nil, errs.ObjectNotFound
|
return nil, errs.ObjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Alias) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
reqPath, err := d.getReqPath(ctx, srcObj)
|
|
||||||
if err == nil {
|
|
||||||
return fs.Rename(ctx, *reqPath, newName)
|
|
||||||
}
|
|
||||||
if errs.IsNotImplement(err) {
|
|
||||||
return errors.New("same-name files cannot be Rename")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Alias) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
reqPath, err := d.getReqPath(ctx, obj)
|
|
||||||
if err == nil {
|
|
||||||
return fs.Remove(ctx, *reqPath)
|
|
||||||
}
|
|
||||||
if errs.IsNotImplement(err) {
|
|
||||||
return errors.New("same-name files cannot be Delete")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Alias)(nil)
|
var _ driver.Driver = (*Alias)(nil)
|
||||||
|
|||||||
@@ -9,25 +9,19 @@ type Addition struct {
|
|||||||
// Usually one of two
|
// Usually one of two
|
||||||
// driver.RootPath
|
// driver.RootPath
|
||||||
// define other
|
// define other
|
||||||
Paths string `json:"paths" required:"true" type:"text"`
|
Paths string `json:"paths" required:"true" type:"text"`
|
||||||
ProtectSameName bool `json:"protect_same_name" default:"true" required:"false" help:"Protects same-name files from Delete or Rename"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "Alias",
|
Name: "Alias",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
NoCache: true,
|
NoCache: true,
|
||||||
NoUpload: true,
|
NoUpload: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
ProxyRangeOption: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &Alias{
|
return &Alias{}
|
||||||
Addition: Addition{
|
|
||||||
ProtectSameName: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
stdpath "path"
|
stdpath "path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/fs"
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/sign"
|
"github.com/alist-org/alist/v3/internal/sign"
|
||||||
@@ -103,49 +102,13 @@ func (d *Alias) link(ctx context.Context, dst, sub string, args model.LinkArgs)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
if common.ShouldProxy(storage, stdpath.Base(sub)) {
|
||||||
link := &model.Link{
|
return &model.Link{
|
||||||
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
URL: fmt.Sprintf("%s/p%s?sign=%s",
|
||||||
common.GetApiUrl(args.HttpReq),
|
common.GetApiUrl(args.HttpReq),
|
||||||
utils.EncodePath(reqPath, true),
|
utils.EncodePath(reqPath, true),
|
||||||
sign.Sign(reqPath)),
|
sign.Sign(reqPath)),
|
||||||
}
|
}, nil
|
||||||
if args.HttpReq != nil && d.ProxyRange {
|
|
||||||
link.RangeReadCloser = common.NoProxyRange
|
|
||||||
}
|
|
||||||
return link, nil
|
|
||||||
}
|
}
|
||||||
link, _, err := fs.Link(ctx, reqPath, args)
|
link, _, err := fs.Link(ctx, reqPath, args)
|
||||||
return link, err
|
return link, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Alias) getReqPath(ctx context.Context, obj model.Obj) (*string, error) {
|
|
||||||
root, sub := d.getRootAndPath(obj.GetPath())
|
|
||||||
if sub == "" || sub == "/" {
|
|
||||||
return nil, errs.NotSupport
|
|
||||||
}
|
|
||||||
dsts, ok := d.pathMap[root]
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
var reqPath string
|
|
||||||
var err error
|
|
||||||
for _, dst := range dsts {
|
|
||||||
reqPath = stdpath.Join(dst, sub)
|
|
||||||
_, err = fs.Get(ctx, reqPath, &fs.GetArgs{NoLog: true})
|
|
||||||
if err == nil {
|
|
||||||
if d.ProtectSameName {
|
|
||||||
if ok {
|
|
||||||
ok = false
|
|
||||||
} else {
|
|
||||||
return nil, errs.NotImplement
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
return &reqPath, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -109,19 +109,11 @@ func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
|
|
||||||
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
var resp common.Resp[FsGetResp]
|
var resp common.Resp[FsGetResp]
|
||||||
// if PassUAToUpsteam is true, then pass the user-agent to the upstream
|
|
||||||
userAgent := base.UserAgent
|
|
||||||
if d.PassUAToUpsteam {
|
|
||||||
userAgent = args.Header.Get("user-agent")
|
|
||||||
if userAgent == "" {
|
|
||||||
userAgent = base.UserAgent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
_, err := d.request("/fs/get", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetBody(FsGetReq{
|
req.SetResult(&resp).SetBody(FsGetReq{
|
||||||
Path: file.GetPath(),
|
Path: file.GetPath(),
|
||||||
Password: d.MetaPassword,
|
Password: d.MetaPassword,
|
||||||
}).SetHeader("user-agent", userAgent)
|
})
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,20 +7,18 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
Address string `json:"url" required:"true"`
|
Address string `json:"url" required:"true"`
|
||||||
MetaPassword string `json:"meta_password"`
|
MetaPassword string `json:"meta_password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
PassUAToUpsteam bool `json:"pass_ua_to_upsteam" default:"true"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
Name: "AList V3",
|
Name: "AList V3",
|
||||||
LocalSort: true,
|
LocalSort: true,
|
||||||
DefaultRoot: "/",
|
DefaultRoot: "/",
|
||||||
CheckStatus: true,
|
CheckStatus: true,
|
||||||
ProxyRangeOption: true,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, streamer model.Fil
|
|||||||
}
|
}
|
||||||
if d.RapidUpload {
|
if d.RapidUpload {
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
buf := bytes.NewBuffer(make([]byte, 0, 1024))
|
||||||
utils.CopyWithBufferN(buf, file, 1024)
|
io.CopyN(buf, file, 1024)
|
||||||
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
|
reqBody["pre_hash"] = utils.HashData(utils.SHA1, buf.Bytes())
|
||||||
if localFile != nil {
|
if localFile != nil {
|
||||||
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
if _, err := localFile.Seek(0, io.SeekStart); err != nil {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (d *AliyundriveOpen) calProofCode(stream model.FileStreamer) (string, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_, err = utils.CopyWithBufferN(buf, reader, length)
|
_, err = io.CopyN(buf, reader, length)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)))
|
count := int(math.Ceil(float64(stream.GetSize()) / float64(partSize)))
|
||||||
createData["part_info_list"] = makePartInfos(count)
|
createData["part_info_list"] = makePartInfos(count)
|
||||||
// rapid upload
|
// rapid upload
|
||||||
rapidUpload := !stream.IsForceStreamUpload() && stream.GetSize() > 100*utils.KB && d.RapidUpload
|
rapidUpload := stream.GetSize() > 100*utils.KB && d.RapidUpload
|
||||||
if rapidUpload {
|
if rapidUpload {
|
||||||
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
log.Debugf("[aliyundrive_open] start cal pre_hash")
|
||||||
// read 1024 bytes to calculate pre hash
|
// read 1024 bytes to calculate pre hash
|
||||||
@@ -242,16 +242,13 @@ func (d *AliyundriveOpen) upload(ctx context.Context, dstDir model.Obj, stream m
|
|||||||
if remain := stream.GetSize() - offset; length > remain {
|
if remain := stream.GetSize() - offset; length > remain {
|
||||||
length = remain
|
length = remain
|
||||||
}
|
}
|
||||||
rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
//rd := utils.NewMultiReadable(io.LimitReader(stream, partSize))
|
||||||
if rapidUpload {
|
rd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
|
||||||
srd, err := stream.RangeRead(http_range.Range{Start: offset, Length: length})
|
if err != nil {
|
||||||
if err != nil {
|
return nil, err
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rd = utils.NewMultiReadable(srd)
|
|
||||||
}
|
}
|
||||||
err = retry.Do(func() error {
|
err = retry.Do(func() error {
|
||||||
rd.Reset()
|
//rd.Reset()
|
||||||
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
|
return d.uploadPart(ctx, rd, createResp.PartInfoList[i])
|
||||||
},
|
},
|
||||||
retry.Attempts(3),
|
retry.Attempts(3),
|
||||||
|
|||||||
@@ -32,13 +32,11 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mega"
|
_ "github.com/alist-org/alist/v3/drivers/mega"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
_ "github.com/alist-org/alist/v3/drivers/mopan"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/netease_music"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
_ "github.com/alist-org/alist/v3/drivers/onedrive_app"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
_ "github.com/alist-org/alist/v3/drivers/pikpak_share"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
_ "github.com/alist-org/alist/v3/drivers/quark_uc"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/quqi"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/s3"
|
_ "github.com/alist-org/alist/v3/drivers/s3"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
_ "github.com/alist-org/alist/v3/drivers/seafile"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/sftp"
|
_ "github.com/alist-org/alist/v3/drivers/sftp"
|
||||||
@@ -46,7 +44,6 @@ import (
|
|||||||
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
_ "github.com/alist-org/alist/v3/drivers/teambition"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
_ "github.com/alist-org/alist/v3/drivers/terabox"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
_ "github.com/alist-org/alist/v3/drivers/thunder"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/thunderx"
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
_ "github.com/alist-org/alist/v3/drivers/trainbit"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
_ "github.com/alist-org/alist/v3/drivers/url_tree"
|
||||||
_ "github.com/alist-org/alist/v3/drivers/uss"
|
_ "github.com/alist-org/alist/v3/drivers/uss"
|
||||||
|
|||||||
@@ -165,16 +165,9 @@ func (d *BaiduNetdisk) PutRapid(ctx context.Context, dstDir model.Obj, stream mo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
|
||||||
newFile.Ctime = stream.CreateTime().Unix()
|
|
||||||
newFile.Mtime = stream.ModTime().Unix()
|
|
||||||
return fileToObj(newFile), nil
|
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) {
|
func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
||||||
// rapid upload
|
// rapid upload
|
||||||
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
if newObj, err := d.PutRapid(ctx, dstDir, stream); err == nil {
|
||||||
@@ -211,7 +204,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
if i == count {
|
if i == count {
|
||||||
byteSize = lastBlockSize
|
byteSize = lastBlockSize
|
||||||
}
|
}
|
||||||
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -252,9 +245,9 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
log.Debugf("%+v", precreateResp)
|
log.Debugf("%+v", precreateResp)
|
||||||
if precreateResp.ReturnType == 2 {
|
if precreateResp.ReturnType == 2 {
|
||||||
//rapid upload, since got md5 match from baidu server
|
//rapid upload, since got md5 match from baidu server
|
||||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
if err != nil {
|
||||||
precreateResp.File.Ctime = ctime
|
return nil, err
|
||||||
precreateResp.File.Mtime = mtime
|
}
|
||||||
return fileToObj(precreateResp.File), nil
|
return fileToObj(precreateResp.File), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,9 +298,6 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// 修复时间,具体原因见 Put 方法注释的 **注意**
|
|
||||||
newFile.Ctime = ctime
|
|
||||||
newFile.Mtime = mtime
|
|
||||||
return fileToObj(newFile), nil
|
return fileToObj(newFile), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
RefreshToken string `json:"refresh_token" required:"true"`
|
RefreshToken string `json:"refresh_token" required:"true"`
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
OrderBy string `json:"order_by" type:"select" options:"name,time,size" default:"name"`
|
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"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
DownloadAPI string `json:"download_api" type:"select" options:"official,crack" default:"official"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
|
||||||
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
CustomCrackUA string `json:"custom_crack_ua" required:"true" default:"netdisk"`
|
||||||
AccessToken string
|
AccessToken string
|
||||||
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
UploadThread string `json:"upload_thread" default:"3" help:"1<=thread<=32"`
|
||||||
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
UploadAPI string `json:"upload_api" default:"https://d.pcs.baidu.com"`
|
||||||
CustomUploadPartSize int64 `json:"custom_upload_part_size" type:"number" default:"0" help:"0 for auto"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -249,9 +249,6 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (d *BaiduNetdisk) getSliceSize() int64 {
|
func (d *BaiduNetdisk) getSliceSize() int64 {
|
||||||
if d.CustomUploadPartSize != 0 {
|
|
||||||
return d.CustomUploadPartSize
|
|
||||||
}
|
|
||||||
switch d.vipType {
|
switch d.vipType {
|
||||||
case 1:
|
case 1:
|
||||||
return VipSliceSize
|
return VipSliceSize
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
|||||||
if i == count {
|
if i == count {
|
||||||
byteSize = lastBlockSize
|
byteSize = lastBlockSize
|
||||||
}
|
}
|
||||||
_, err := utils.CopyWithBufferN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
_, err := io.CopyN(io.MultiWriter(fileMd5H, sliceMd5H, slicemd5H2Write), tempFile, byteSize)
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ func (d *ChaoXing) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = utils.CopyWithBuffer(filePart, stream)
|
_, err = io.Copy(filePart, stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package chaoxing
|
package chaoxing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
@@ -90,59 +88,44 @@ type UserAuth struct {
|
|||||||
} `json:"operationAuth"`
|
} `json:"operationAuth"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手机端学习通上传的文件的json内容(content字段)与网页端上传的有所不同
|
|
||||||
// 网页端json `"puid": 54321, "size": 12345`
|
|
||||||
// 手机端json `"puid": "54321". "size": "12345"`
|
|
||||||
type int_str int
|
|
||||||
|
|
||||||
// json 字符串数字和纯数字解析
|
|
||||||
func (ios *int_str) UnmarshalJSON(data []byte) error {
|
|
||||||
intValue, err := strconv.Atoi(string(bytes.Trim(data, "\"")))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*ios = int_str(intValue)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Cataid int `json:"cataid"`
|
Cataid int `json:"cataid"`
|
||||||
Cfid int `json:"cfid"`
|
Cfid int `json:"cfid"`
|
||||||
Content struct {
|
Content struct {
|
||||||
Cfid int `json:"cfid"`
|
Cfid int `json:"cfid"`
|
||||||
Pid int `json:"pid"`
|
Pid int `json:"pid"`
|
||||||
FolderName string `json:"folderName"`
|
FolderName string `json:"folderName"`
|
||||||
ShareType int `json:"shareType"`
|
ShareType int `json:"shareType"`
|
||||||
Preview string `json:"preview"`
|
Preview string `json:"preview"`
|
||||||
Filetype string `json:"filetype"`
|
Filetype string `json:"filetype"`
|
||||||
PreviewURL string `json:"previewUrl"`
|
PreviewURL string `json:"previewUrl"`
|
||||||
IsImg bool `json:"isImg"`
|
IsImg bool `json:"isImg"`
|
||||||
ParentPath string `json:"parentPath"`
|
ParentPath string `json:"parentPath"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
Suffix string `json:"suffix"`
|
Suffix string `json:"suffix"`
|
||||||
Duration int `json:"duration"`
|
Duration int `json:"duration"`
|
||||||
Pantype string `json:"pantype"`
|
Pantype string `json:"pantype"`
|
||||||
Puid int_str `json:"puid"`
|
Puid int `json:"puid"`
|
||||||
Filepath string `json:"filepath"`
|
Filepath string `json:"filepath"`
|
||||||
Crc string `json:"crc"`
|
Crc string `json:"crc"`
|
||||||
Isfile bool `json:"isfile"`
|
Isfile bool `json:"isfile"`
|
||||||
Residstr string `json:"residstr"`
|
Residstr string `json:"residstr"`
|
||||||
ObjectID string `json:"objectId"`
|
ObjectID string `json:"objectId"`
|
||||||
Extinfo string `json:"extinfo"`
|
Extinfo string `json:"extinfo"`
|
||||||
Thumbnail string `json:"thumbnail"`
|
Thumbnail string `json:"thumbnail"`
|
||||||
Creator int `json:"creator"`
|
Creator int `json:"creator"`
|
||||||
ResTypeValue int `json:"resTypeValue"`
|
ResTypeValue int `json:"resTypeValue"`
|
||||||
UploadDateFormat string `json:"uploadDateFormat"`
|
UploadDateFormat string `json:"uploadDateFormat"`
|
||||||
DisableOpt bool `json:"disableOpt"`
|
DisableOpt bool `json:"disableOpt"`
|
||||||
DownPath string `json:"downPath"`
|
DownPath string `json:"downPath"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
Topsort int `json:"topsort"`
|
Topsort int `json:"topsort"`
|
||||||
Restype string `json:"restype"`
|
Restype string `json:"restype"`
|
||||||
Size int_str `json:"size"`
|
Size int `json:"size"`
|
||||||
UploadDate int64 `json:"uploadDate"`
|
UploadDate string `json:"uploadDate"`
|
||||||
FileSize string `json:"fileSize"`
|
FileSize string `json:"fileSize"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
FileID string `json:"fileId"`
|
FileID string `json:"fileId"`
|
||||||
} `json:"content"`
|
} `json:"content"`
|
||||||
CreatorID int `json:"creatorId"`
|
CreatorID int `json:"creatorId"`
|
||||||
DesID string `json:"des_id"`
|
DesID string `json:"des_id"`
|
||||||
@@ -221,6 +204,7 @@ type UploadFileDataRsp struct {
|
|||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type UploadDoneParam struct {
|
type UploadDoneParam struct {
|
||||||
Cataid string `json:"cataid"`
|
Cataid string `json:"cataid"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
@@ -265,7 +249,10 @@ func fileToObj(f File) *model.Object {
|
|||||||
IsFolder: true,
|
IsFolder: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
paserTime := time.UnixMilli(f.Content.UploadDate)
|
paserTime, err := time.Parse("2006-01-02 15:04", f.Content.UploadDate)
|
||||||
|
if err != nil {
|
||||||
|
paserTime = time.Now()
|
||||||
|
}
|
||||||
return &model.Object{
|
return &model.Object{
|
||||||
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
|
ID: fmt.Sprintf("%d$%s", f.ID, f.Content.FileID),
|
||||||
Name: f.Content.Name,
|
Name: f.Content.Name,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (d *ChaoXing) GetFiles(parent string) ([]File, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.Result != 1 {
|
if resp.Result != 1 {
|
||||||
msg := fmt.Sprintf("error code is:%d", resp.Result)
|
msg:=fmt.Sprintf("error code is:%d", resp.Result)
|
||||||
return nil, errors.New(msg)
|
return nil, errors.New(msg)
|
||||||
}
|
}
|
||||||
if len(resp.List) > 0 {
|
if len(resp.List) > 0 {
|
||||||
@@ -97,12 +97,8 @@ func (d *ChaoXing) GetFiles(parent string) ([]File, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, file := range resps.List {
|
if len(resps.List) > 0 {
|
||||||
// 手机端超星上传的文件没有fileID字段,但ObjectID与fileID相同,可代替
|
files = append(files, resps.List...)
|
||||||
if file.Content.FileID == "" {
|
|
||||||
file.Content.FileID = file.Content.ObjectID
|
|
||||||
}
|
|
||||||
files = append(files, file)
|
|
||||||
}
|
}
|
||||||
return files, nil
|
return files, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ func (d *Cloudreve) Link(ctx context.Context, file model.Obj, args model.LinkArg
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(dUrl, "/api") {
|
|
||||||
dUrl = d.Address + dUrl
|
|
||||||
}
|
|
||||||
return &model.Link{
|
return &model.Link{
|
||||||
URL: dUrl,
|
URL: dUrl,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package crypt
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/alist-org/alist/v3/internal/stream"
|
||||||
"io"
|
"io"
|
||||||
stdpath "path"
|
stdpath "path"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -13,7 +14,6 @@ import (
|
|||||||
"github.com/alist-org/alist/v3/internal/fs"
|
"github.com/alist-org/alist/v3/internal/fs"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
"github.com/alist-org/alist/v3/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/http_range"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
"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
|
// discarding hash as it's encrypted
|
||||||
}
|
}
|
||||||
if d.Thumbnail && thumb == "" {
|
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 {
|
if !ok && !d.Thumbnail {
|
||||||
result = append(result, &objRes)
|
result = append(result, &objRes)
|
||||||
@@ -389,11 +389,10 @@ func (d *Crypt) Put(ctx context.Context, dstDir model.Obj, streamer model.FileSt
|
|||||||
Modified: streamer.ModTime(),
|
Modified: streamer.ModTime(),
|
||||||
IsFolder: streamer.IsDir(),
|
IsFolder: streamer.IsDir(),
|
||||||
},
|
},
|
||||||
Reader: wrappedIn,
|
Reader: wrappedIn,
|
||||||
Mimetype: "application/octet-stream",
|
Mimetype: "application/octet-stream",
|
||||||
WebPutAsTask: streamer.NeedStore(),
|
WebPutAsTask: streamer.NeedStore(),
|
||||||
ForceStreamUpload: true,
|
Exist: streamer.GetExist(),
|
||||||
Exist: streamer.GetExist(),
|
|
||||||
}
|
}
|
||||||
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
|
err = op.Put(ctx, d.remoteStorage, dstDirActualPath, streamOut, up, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -45,25 +45,7 @@ func (d *Dropbox) Init(ctx context.Context) error {
|
|||||||
if result != query {
|
if result != query {
|
||||||
return fmt.Errorf("failed to check user: %s", string(res))
|
return fmt.Errorf("failed to check user: %s", string(res))
|
||||||
}
|
}
|
||||||
d.RootNamespaceId, err = d.GetRootNamespaceId(ctx)
|
return nil
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Dropbox) GetRootNamespaceId(ctx context.Context) (string, error) {
|
|
||||||
res, err := d.request("/2/users/get_current_account", http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(nil)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
var currentAccountResp CurrentAccountResp
|
|
||||||
err = utils.Json.Unmarshal(res, ¤tAccountResp)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
rootNamespaceId := currentAccountResp.RootInfo.RootNamespaceId
|
|
||||||
return rootNamespaceId, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dropbox) Drop(ctx context.Context) error {
|
func (d *Dropbox) Drop(ctx context.Context) error {
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ type Addition struct {
|
|||||||
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
ClientID string `json:"client_id" required:"false" help:"Keep it empty if you don't have one"`
|
||||||
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
ClientSecret string `json:"client_secret" required:"false" help:"Keep it empty if you don't have one"`
|
||||||
|
|
||||||
AccessToken string
|
AccessToken string
|
||||||
RootNamespaceId string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -23,13 +23,6 @@ type RefreshTokenErrorResp struct {
|
|||||||
ErrorDescription string `json:"error_description"`
|
ErrorDescription string `json:"error_description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CurrentAccountResp struct {
|
|
||||||
RootInfo struct {
|
|
||||||
RootNamespaceId string `json:"root_namespace_id"`
|
|
||||||
HomeNamespaceId string `json:"home_namespace_id"`
|
|
||||||
} `json:"root_info"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Tag string `json:".tag"`
|
Tag string `json:".tag"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -46,22 +46,12 @@ func (d *Dropbox) refreshToken() error {
|
|||||||
func (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
func (d *Dropbox) request(uri, method string, callback base.ReqCallback, retry ...bool) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
if d.RootNamespaceId != "" {
|
if method == http.MethodPost {
|
||||||
apiPathRootJson, err := utils.Json.MarshalToString(map[string]interface{}{
|
req.SetHeader("Content-Type", "application/json")
|
||||||
".tag": "root",
|
|
||||||
"root": d.RootNamespaceId,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.SetHeader("Dropbox-API-Path-Root", apiPathRootJson)
|
|
||||||
}
|
}
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
}
|
}
|
||||||
if method == http.MethodPost && req.Body != nil {
|
|
||||||
req.SetHeader("Content-Type", "application/json")
|
|
||||||
}
|
|
||||||
var e ErrorResp
|
var e ErrorResp
|
||||||
req.SetError(&e)
|
req.SetError(&e)
|
||||||
res, err := req.Execute(method, d.base+uri)
|
res, err := req.Execute(method, d.base+uri)
|
||||||
|
|||||||
@@ -58,33 +58,9 @@ func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkA
|
|||||||
URL: f.BaseURL + "=d",
|
URL: f.BaseURL + "=d",
|
||||||
}, nil
|
}, nil
|
||||||
} else if strings.Contains(f.MimeType, "video/") {
|
} else if strings.Contains(f.MimeType, "video/") {
|
||||||
var width, height int
|
return &model.Link{
|
||||||
|
URL: f.BaseURL + "=dv",
|
||||||
fmt.Sscanf(f.MediaMetadata.Width, "%d", &width)
|
}, nil
|
||||||
fmt.Sscanf(f.MediaMetadata.Height, "%d", &height)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
// 1080P
|
|
||||||
case width == 1920 && height == 1080:
|
|
||||||
return &model.Link{
|
|
||||||
URL: f.BaseURL + "=m37",
|
|
||||||
}, nil
|
|
||||||
// 720P
|
|
||||||
case width == 1280 && height == 720:
|
|
||||||
return &model.Link{
|
|
||||||
URL: f.BaseURL + "=m22",
|
|
||||||
}, nil
|
|
||||||
// 360P
|
|
||||||
case width == 640 && height == 360:
|
|
||||||
return &model.Link{
|
|
||||||
URL: f.BaseURL + "=m18",
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return &model.Link{
|
|
||||||
URL: f.BaseURL + "=dv",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return &model.Link{}, nil
|
return &model.Link{}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (d *GooglePhoto) getMedia(id string) (MediaItem, error) {
|
|||||||
var resp MediaItem
|
var resp MediaItem
|
||||||
|
|
||||||
query := map[string]string{
|
query := map[string]string{
|
||||||
"fields": "mediaMetadata,baseUrl,mimeType",
|
"fields": "baseUrl,mimeType",
|
||||||
}
|
}
|
||||||
_, err := d.request(fmt.Sprintf("https://photoslibrary.googleapis.com/v1/mediaItems/%s", id), http.MethodGet, func(req *resty.Request) {
|
_, err := d.request(fmt.Sprintf("https://photoslibrary.googleapis.com/v1/mediaItems/%s", id), http.MethodGet, func(req *resty.Request) {
|
||||||
req.SetQueryParams(query)
|
req.SetQueryParams(query)
|
||||||
|
|||||||
@@ -30,12 +30,10 @@ type ILanZou struct {
|
|||||||
userID string
|
userID string
|
||||||
account string
|
account string
|
||||||
upClient *resty.Client
|
upClient *resty.Client
|
||||||
conf Conf
|
|
||||||
config driver.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ILanZou) Config() driver.Config {
|
func (d *ILanZou) Config() driver.Config {
|
||||||
return d.config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ILanZou) GetAddition() driver.Additional {
|
func (d *ILanZou) GetAddition() driver.Additional {
|
||||||
@@ -114,7 +112,7 @@ func (d *ILanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
u, err := url.Parse(d.conf.base + "/" + d.conf.unproved + "/file/redirect")
|
u, err := url.Parse("https://api.ilanzou.com/unproved/file/redirect")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -123,47 +121,27 @@ func (d *ILanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
|
|||||||
query.Set("devType", "6")
|
query.Set("devType", "6")
|
||||||
query.Set("devCode", d.UUID)
|
query.Set("devCode", d.UUID)
|
||||||
query.Set("devModel", "chrome")
|
query.Set("devModel", "chrome")
|
||||||
query.Set("devVersion", d.conf.devVersion)
|
query.Set("devVersion", "120")
|
||||||
query.Set("appVersion", "")
|
query.Set("appVersion", "")
|
||||||
ts, err := getTimestamp(d.conf.secret)
|
ts, err := getTimestamp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
query.Set("timestamp", ts)
|
query.Set("timestamp", ts)
|
||||||
query.Set("appToken", d.Token)
|
//query.Set("appToken", d.Token)
|
||||||
query.Set("enable", "1")
|
query.Set("enable", "1")
|
||||||
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), d.conf.secret)
|
downloadId, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%s", file.GetID(), d.userID)), AesSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
query.Set("downloadId", hex.EncodeToString(downloadId))
|
query.Set("downloadId", hex.EncodeToString(downloadId))
|
||||||
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), time.Now().UnixMilli())), d.conf.secret)
|
auth, err := mopan.AesEncrypt([]byte(fmt.Sprintf("%s|%d", file.GetID(), time.Now().UnixMilli())), AesSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
query.Set("auth", hex.EncodeToString(auth))
|
query.Set("auth", hex.EncodeToString(auth))
|
||||||
u.RawQuery = query.Encode()
|
u.RawQuery = query.Encode()
|
||||||
realURL := u.String()
|
link := model.Link{URL: u.String()}
|
||||||
// get the url after redirect
|
|
||||||
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
|
|
||||||
//"Origin": d.conf.site,
|
|
||||||
"Referer": d.conf.site + "/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
|
||||||
}).Get(realURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if res.StatusCode() == 302 {
|
|
||||||
realURL = res.Header().Get("location")
|
|
||||||
} else {
|
|
||||||
contentLengthStr := res.Header().Get("Content-Length")
|
|
||||||
contentLength, err := strconv.Atoi(contentLengthStr)
|
|
||||||
if err != nil || contentLength == 0 || contentLength > 1024*10 {
|
|
||||||
return nil, fmt.Errorf("redirect failed, status: %d", res.StatusCode())
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("redirect failed, content: %s", res.String())
|
|
||||||
}
|
|
||||||
link := model.Link{URL: realURL}
|
|
||||||
return &link, nil
|
return &link, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,7 +255,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
defer func() {
|
defer func() {
|
||||||
_ = tempFile.Close()
|
_ = tempFile.Close()
|
||||||
}()
|
}()
|
||||||
if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
|
if _, err = io.Copy(h, tempFile); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
_, err = tempFile.Seek(0, io.SeekStart)
|
_, err = tempFile.Seek(0, io.SeekStart)
|
||||||
@@ -303,7 +281,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
key := fmt.Sprintf("disk/%d/%d/%d/%s/%016d", now.Year(), now.Month(), now.Day(), d.account, now.UnixMilli())
|
key := fmt.Sprintf("disk/%d/%d/%d/%s/%016d", now.Year(), now.Month(), now.Day(), d.account, now.UnixMilli())
|
||||||
var token string
|
var token string
|
||||||
if stream.GetSize() <= DefaultPartSize {
|
if stream.GetSize() > DefaultPartSize {
|
||||||
res, err := d.upClient.R().SetMultipartFormData(map[string]string{
|
res, err := d.upClient.R().SetMultipartFormData(map[string]string{
|
||||||
"token": upToken,
|
"token": upToken,
|
||||||
"key": key,
|
"key": key,
|
||||||
@@ -316,7 +294,7 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
token = utils.Json.Get(res.Body(), "token").ToString()
|
token = utils.Json.Get(res.Body(), "token").ToString()
|
||||||
} else {
|
} else {
|
||||||
keyBase64 := base64.URLEncoding.EncodeToString([]byte(key))
|
keyBase64 := base64.URLEncoding.EncodeToString([]byte(key))
|
||||||
res, err := d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads", d.conf.bucket, keyBase64))
|
res, err := d.upClient.R().Post(fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads", keyBase64))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -324,8 +302,8 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
parts := make([]Part, 0)
|
parts := make([]Part, 0)
|
||||||
partNum := (stream.GetSize() + DefaultPartSize - 1) / DefaultPartSize
|
partNum := (stream.GetSize() + DefaultPartSize - 1) / DefaultPartSize
|
||||||
for i := 1; i <= int(partNum); i++ {
|
for i := 1; i <= int(partNum); i++ {
|
||||||
u := fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s/%d", d.conf.bucket, keyBase64, uploadId, i)
|
u := fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads/%s/%d", keyBase64, uploadId, i)
|
||||||
res, err = d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).SetBody(io.LimitReader(tempFile, DefaultPartSize)).Put(u)
|
res, err = d.upClient.R().SetBody(io.LimitReader(tempFile, DefaultPartSize)).Put(u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -335,10 +313,10 @@ func (d *ILanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
ETag: etag,
|
ETag: etag,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
res, err = d.upClient.R().SetHeader("Authorization", "UpToken "+upToken).SetBody(base.Json{
|
res, err = d.upClient.R().SetBody(base.Json{
|
||||||
"fnmae": stream.GetName(),
|
"fnmae": stream.GetName(),
|
||||||
"parts": parts,
|
"parts": parts,
|
||||||
}).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/%s/objects/%s/uploads/%s", d.conf.bucket, keyBase64, uploadId))
|
}).Post(fmt.Sprintf("https://upload.qiniup.com/buckets/wpanstore-lanzou/objects/%s/uploads/%s", keyBase64, uploadId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,67 +14,22 @@ type Addition struct {
|
|||||||
UUID string
|
UUID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Conf struct {
|
var config = driver.Config{
|
||||||
base string
|
Name: "ILanZou",
|
||||||
secret []byte
|
LocalSort: false,
|
||||||
bucket string
|
OnlyLocal: false,
|
||||||
unproved string
|
OnlyProxy: false,
|
||||||
proved string
|
NoCache: false,
|
||||||
devVersion string
|
NoUpload: false,
|
||||||
site string
|
NeedMs: false,
|
||||||
|
DefaultRoot: "0",
|
||||||
|
CheckStatus: false,
|
||||||
|
Alert: "",
|
||||||
|
NoOverwriteUpload: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
op.RegisterDriver(func() driver.Driver {
|
||||||
return &ILanZou{
|
return &ILanZou{}
|
||||||
config: driver.Config{
|
|
||||||
Name: "ILanZou",
|
|
||||||
LocalSort: false,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: false,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "0",
|
|
||||||
CheckStatus: false,
|
|
||||||
Alert: "",
|
|
||||||
NoOverwriteUpload: false,
|
|
||||||
},
|
|
||||||
conf: Conf{
|
|
||||||
base: "https://api.ilanzou.com",
|
|
||||||
secret: []byte("lanZouY-disk-app"),
|
|
||||||
bucket: "wpanstore-lanzou",
|
|
||||||
unproved: "unproved",
|
|
||||||
proved: "proved",
|
|
||||||
devVersion: "125",
|
|
||||||
site: "https://www.ilanzou.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &ILanZou{
|
|
||||||
config: driver.Config{
|
|
||||||
Name: "FeijiPan",
|
|
||||||
LocalSort: false,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: false,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "0",
|
|
||||||
CheckStatus: false,
|
|
||||||
Alert: "",
|
|
||||||
NoOverwriteUpload: false,
|
|
||||||
},
|
|
||||||
conf: Conf{
|
|
||||||
base: "https://api.feijipan.com",
|
|
||||||
secret: []byte("dingHao-disk-app"),
|
|
||||||
bucket: "wpanstore",
|
|
||||||
unproved: "ws",
|
|
||||||
proved: "app",
|
|
||||||
devVersion: "125",
|
|
||||||
site: "https://www.feijipan.com",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Base = "https://api.ilanzou.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AesSecret = []byte("lanZouY-disk-app")
|
||||||
|
)
|
||||||
|
|
||||||
func (d *ILanZou) login() error {
|
func (d *ILanZou) login() error {
|
||||||
res, err := d.unproved("/login", http.MethodPost, func(req *resty.Request) {
|
res, err := d.unproved("/login", http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(base.Json{
|
req.SetBody(base.Json{
|
||||||
@@ -31,10 +39,10 @@ func (d *ILanZou) login() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTimestamp(secret []byte) (string, error) {
|
func getTimestamp() (string, error) {
|
||||||
ts := time.Now().UnixMilli()
|
ts := time.Now().UnixMilli()
|
||||||
tsStr := strconv.FormatInt(ts, 10)
|
tsStr := strconv.FormatInt(ts, 10)
|
||||||
res, err := mopan.AesEncrypt([]byte(tsStr), secret)
|
res, err := mopan.AesEncrypt([]byte(tsStr), AesSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -43,7 +51,7 @@ func getTimestamp(secret []byte) (string, error) {
|
|||||||
|
|
||||||
func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, proved bool, retry ...bool) ([]byte, error) {
|
func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, proved bool, retry ...bool) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
ts, err := getTimestamp(d.conf.secret)
|
ts, err := getTimestamp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -52,24 +60,19 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
|
|||||||
"devType": "6",
|
"devType": "6",
|
||||||
"devCode": d.UUID,
|
"devCode": d.UUID,
|
||||||
"devModel": "chrome",
|
"devModel": "chrome",
|
||||||
"devVersion": d.conf.devVersion,
|
"devVersion": "120",
|
||||||
"appVersion": "",
|
"appVersion": "",
|
||||||
"timestamp": ts,
|
"timestamp": ts,
|
||||||
//"appToken": d.Token,
|
//"appToken": d.Token,
|
||||||
"extra": "2",
|
"extra": "2",
|
||||||
})
|
})
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"Origin": d.conf.site,
|
|
||||||
"Referer": d.conf.site + "/",
|
|
||||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
|
|
||||||
})
|
|
||||||
if proved {
|
if proved {
|
||||||
req.SetQueryParam("appToken", d.Token)
|
req.SetQueryParam("appToken", d.Token)
|
||||||
}
|
}
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
}
|
}
|
||||||
res, err := req.Execute(method, d.conf.base+pathname)
|
res, err := req.Execute(method, Base+pathname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if res != nil {
|
if res != nil {
|
||||||
log.Errorf("[iLanZou] request error: %s", res.String())
|
log.Errorf("[iLanZou] request error: %s", res.String())
|
||||||
@@ -94,9 +97,9 @@ func (d *ILanZou) request(pathname, method string, callback base.ReqCallback, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *ILanZou) unproved(pathname, method string, callback base.ReqCallback) ([]byte, error) {
|
func (d *ILanZou) unproved(pathname, method string, callback base.ReqCallback) ([]byte, error) {
|
||||||
return d.request("/"+d.conf.unproved+pathname, method, callback, false)
|
return d.request("/unproved"+pathname, method, callback, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *ILanZou) proved(pathname, method string, callback base.ReqCallback) ([]byte, error) {
|
func (d *ILanZou) proved(pathname, method string, callback base.ReqCallback) ([]byte, error) {
|
||||||
return d.request("/"+d.conf.proved+pathname, method, callback, true)
|
return d.request("/proved"+pathname, method, callback, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ func (d *IPFS) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
|||||||
for _, file := range dirs {
|
for _, file := range dirs {
|
||||||
gateurl := *d.gateURL
|
gateurl := *d.gateURL
|
||||||
gateurl.Path = "ipfs/" + file.Hash
|
gateurl.Path = "ipfs/" + file.Hash
|
||||||
gateurl.RawQuery = "filename=" + url.PathEscape(file.Name)
|
gateurl.RawQuery = "filename=" + file.Name
|
||||||
objlist = append(objlist, &model.ObjectURL{
|
objlist = append(objlist, &model.ObjectURL{
|
||||||
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
|
Object: model.Object{ID: file.Hash, Name: file.Name, Size: int64(file.Size), IsFolder: file.Type == 1},
|
||||||
Url: model.Url{Url: gateurl.String()},
|
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) {
|
func (d *IPFS) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + url.PathEscape(file.GetName())
|
link := d.Gateway + "/ipfs/" + file.GetID() + "/?filename=" + file.GetName()
|
||||||
return &model.Link{URL: link}, nil
|
return &model.Link{URL: link}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
// +build linux darwin windows
|
|
||||||
// +build amd64 arm64
|
|
||||||
|
|
||||||
package drivers
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "github.com/alist-org/alist/v3/drivers/lark"
|
|
||||||
)
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
package lark
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"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/ipfs/boxo/path"
|
|
||||||
lark "github.com/larksuite/oapi-sdk-go/v3"
|
|
||||||
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
||||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
|
||||||
"golang.org/x/time/rate"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Lark struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
|
|
||||||
client *lark.Client
|
|
||||||
rootFolderToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) GetAddition() driver.Additional {
|
|
||||||
return &c.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Init(ctx context.Context) error {
|
|
||||||
c.client = lark.NewClient(c.AppId, c.AppSecret, lark.WithTokenCache(newTokenCache()))
|
|
||||||
|
|
||||||
paths := path.SplitList(c.RootFolderPath)
|
|
||||||
token := ""
|
|
||||||
|
|
||||||
var ok bool
|
|
||||||
var file *larkdrive.File
|
|
||||||
for _, p := range paths {
|
|
||||||
if p == "" {
|
|
||||||
token = ""
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
ok, file, err = resp.Next()
|
|
||||||
if !ok {
|
|
||||||
return errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if *file.Type == "folder" && *file.Name == p {
|
|
||||||
token = *file.Token
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.rootFolderToken = token
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
token, ok := c.getObjToken(ctx, dir.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
if token == emptyFolderToken {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.client.Drive.File.ListByIterator(ctx, larkdrive.NewListFileReqBuilder().FolderToken(token).Build())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = false
|
|
||||||
var file *larkdrive.File
|
|
||||||
var res []model.Obj
|
|
||||||
|
|
||||||
for {
|
|
||||||
ok, file, err = resp.Next()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
modifiedUnix, _ := strconv.ParseInt(*file.ModifiedTime, 10, 64)
|
|
||||||
createdUnix, _ := strconv.ParseInt(*file.CreatedTime, 10, 64)
|
|
||||||
|
|
||||||
f := model.Object{
|
|
||||||
ID: *file.Token,
|
|
||||||
Path: path.Join([]string{c.RootFolderPath, dir.GetPath(), *file.Name}),
|
|
||||||
Name: *file.Name,
|
|
||||||
Size: 0,
|
|
||||||
Modified: time.Unix(modifiedUnix, 0),
|
|
||||||
Ctime: time.Unix(createdUnix, 0),
|
|
||||||
IsFolder: *file.Type == "folder",
|
|
||||||
}
|
|
||||||
res = append(res, &f)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
token, ok := c.getObjToken(ctx, file.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.client.GetTenantAccessTokenBySelfBuiltApp(ctx, &larkcore.SelfBuiltTenantAccessTokenReq{
|
|
||||||
AppID: c.AppId,
|
|
||||||
AppSecret: c.AppSecret,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !c.ExternalMode {
|
|
||||||
accessToken := resp.TenantAccessToken
|
|
||||||
|
|
||||||
url := fmt.Sprintf("https://open.feishu.cn/open-apis/drive/v1/files/%s/download", token)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
|
|
||||||
req.Header.Set("Range", "bytes=0-1")
|
|
||||||
|
|
||||||
ar, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ar.StatusCode != http.StatusPartialContent {
|
|
||||||
return nil, errors.New("failed to get download link")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Link{
|
|
||||||
URL: url,
|
|
||||||
Header: http.Header{
|
|
||||||
"Authorization": []string{fmt.Sprintf("Bearer %s", accessToken)},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
} else {
|
|
||||||
url := path.Join([]string{c.TenantUrlPrefix, "file", token})
|
|
||||||
|
|
||||||
return &model.Link{
|
|
||||||
URL: url,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
|
||||||
token, ok := c.getObjToken(ctx, parentDir.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := larkdrive.NewCreateFolderFilePathReqBodyBuilder().FolderToken(token).Name(dirName).Build()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := c.client.Drive.File.CreateFolder(ctx,
|
|
||||||
larkdrive.NewCreateFolderFileReqBuilder().Body(body).Build())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return nil, errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Object{
|
|
||||||
ID: *resp.Data.Token,
|
|
||||||
Path: path.Join([]string{c.RootFolderPath, parentDir.GetPath(), dirName}),
|
|
||||||
Name: dirName,
|
|
||||||
Size: 0,
|
|
||||||
IsFolder: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
|
||||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
req := larkdrive.NewMoveFileReqBuilder().
|
|
||||||
Body(larkdrive.NewMoveFileReqBodyBuilder().
|
|
||||||
Type("file").
|
|
||||||
FolderToken(dstDirToken).
|
|
||||||
Build()).FileToken(srcToken).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
resp, err := c.client.Drive.File.Move(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return nil, errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
|
||||||
// TODO rename obj, optional
|
|
||||||
return nil, errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
|
||||||
srcToken, ok := c.getObjToken(ctx, srcObj.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
dstDirToken, ok := c.getObjToken(ctx, dstDir.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
req := larkdrive.NewCopyFileReqBuilder().
|
|
||||||
Body(larkdrive.NewCopyFileReqBodyBuilder().
|
|
||||||
Name(srcObj.GetName()).
|
|
||||||
Type("file").
|
|
||||||
FolderToken(dstDirToken).
|
|
||||||
Build()).FileToken(srcToken).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
resp, err := c.client.Drive.File.Copy(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return nil, errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Lark) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
token, ok := c.getObjToken(ctx, obj.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
req := larkdrive.NewDeleteFileReqBuilder().
|
|
||||||
FileToken(token).
|
|
||||||
Type("file").
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
resp, err := c.client.Drive.File.Delete(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var uploadLimit = rate.NewLimiter(rate.Every(time.Second), 5)
|
|
||||||
|
|
||||||
func (c *Lark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
|
||||||
token, ok := c.getObjToken(ctx, dstDir.GetPath())
|
|
||||||
if !ok {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare
|
|
||||||
req := larkdrive.NewUploadPrepareFileReqBuilder().
|
|
||||||
FileUploadInfo(larkdrive.NewFileUploadInfoBuilder().
|
|
||||||
FileName(stream.GetName()).
|
|
||||||
ParentType(`explorer`).
|
|
||||||
ParentNode(token).
|
|
||||||
Size(int(stream.GetSize())).
|
|
||||||
Build()).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
uploadLimit.Wait(ctx)
|
|
||||||
resp, err := c.client.Drive.File.UploadPrepare(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return nil, errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadId := *resp.Data.UploadId
|
|
||||||
blockSize := *resp.Data.BlockSize
|
|
||||||
blockCount := *resp.Data.BlockNum
|
|
||||||
|
|
||||||
// upload
|
|
||||||
for i := 0; i < blockCount; i++ {
|
|
||||||
length := int64(blockSize)
|
|
||||||
if i == blockCount-1 {
|
|
||||||
length = stream.GetSize() - int64(i*blockSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
reader := io.LimitReader(stream, length)
|
|
||||||
|
|
||||||
req := larkdrive.NewUploadPartFileReqBuilder().
|
|
||||||
Body(larkdrive.NewUploadPartFileReqBodyBuilder().
|
|
||||||
UploadId(uploadId).
|
|
||||||
Seq(i).
|
|
||||||
Size(int(length)).
|
|
||||||
File(reader).
|
|
||||||
Build()).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
uploadLimit.Wait(ctx)
|
|
||||||
resp, err := c.client.Drive.File.UploadPart(ctx, req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !resp.Success() {
|
|
||||||
return nil, errors.New(resp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
up(float64(i) / float64(blockCount))
|
|
||||||
}
|
|
||||||
|
|
||||||
//close
|
|
||||||
closeReq := larkdrive.NewUploadFinishFileReqBuilder().
|
|
||||||
Body(larkdrive.NewUploadFinishFileReqBodyBuilder().
|
|
||||||
UploadId(uploadId).
|
|
||||||
BlockNum(blockCount).
|
|
||||||
Build()).
|
|
||||||
Build()
|
|
||||||
|
|
||||||
// 发起请求
|
|
||||||
closeResp, err := c.client.Drive.File.UploadFinish(ctx, closeReq)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !closeResp.Success() {
|
|
||||||
return nil, errors.New(closeResp.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Object{
|
|
||||||
ID: *closeResp.Data.FileToken,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Lark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Lark)(nil)
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package lark
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
// Usually one of two
|
|
||||||
driver.RootPath
|
|
||||||
// define other
|
|
||||||
AppId string `json:"app_id" type:"text" help:"app id"`
|
|
||||||
AppSecret string `json:"app_secret" type:"text" help:"app secret"`
|
|
||||||
ExternalMode bool `json:"external_mode" type:"bool" help:"external mode"`
|
|
||||||
TenantUrlPrefix string `json:"tenant_url_prefix" type:"text" help:"tenant url prefix"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Lark",
|
|
||||||
LocalSort: false,
|
|
||||||
OnlyLocal: false,
|
|
||||||
OnlyProxy: false,
|
|
||||||
NoCache: false,
|
|
||||||
NoUpload: false,
|
|
||||||
NeedMs: false,
|
|
||||||
DefaultRoot: "/",
|
|
||||||
CheckStatus: false,
|
|
||||||
Alert: "",
|
|
||||||
NoOverwriteUpload: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Lark{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package lark
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/Xhofe/go-cache"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TokenCache struct {
|
|
||||||
cache.ICache[string]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TokenCache) Set(_ context.Context, key string, value string, expireTime time.Duration) error {
|
|
||||||
t.ICache.Set(key, value, cache.WithEx[string](expireTime))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *TokenCache) Get(_ context.Context, key string) (string, error) {
|
|
||||||
v, ok := t.ICache.Get(key)
|
|
||||||
if ok {
|
|
||||||
return v, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTokenCache() *TokenCache {
|
|
||||||
c := cache.NewMemCache[string]()
|
|
||||||
|
|
||||||
return &TokenCache{c}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package lark
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/Xhofe/go-cache"
|
|
||||||
larkdrive "github.com/larksuite/oapi-sdk-go/v3/service/drive/v1"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"path"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const objTokenCacheDuration = 5 * time.Minute
|
|
||||||
const emptyFolderToken = "empty"
|
|
||||||
|
|
||||||
var objTokenCache = cache.NewMemCache[string]()
|
|
||||||
var exOpts = cache.WithEx[string](objTokenCacheDuration)
|
|
||||||
|
|
||||||
func (c *Lark) getObjToken(ctx context.Context, folderPath string) (string, bool) {
|
|
||||||
if token, ok := objTokenCache.Get(folderPath); ok {
|
|
||||||
return token, true
|
|
||||||
}
|
|
||||||
|
|
||||||
dir, name := path.Split(folderPath)
|
|
||||||
// strip the last slash of dir if it exists
|
|
||||||
if len(dir) > 0 && dir[len(dir)-1] == '/' {
|
|
||||||
dir = dir[:len(dir)-1]
|
|
||||||
}
|
|
||||||
if name == "" {
|
|
||||||
return c.rootFolderToken, true
|
|
||||||
}
|
|
||||||
|
|
||||||
var parentToken string
|
|
||||||
var found bool
|
|
||||||
parentToken, found = c.getObjToken(ctx, dir)
|
|
||||||
if !found {
|
|
||||||
return emptyFolderToken, false
|
|
||||||
}
|
|
||||||
|
|
||||||
req := larkdrive.NewListFileReqBuilder().FolderToken(parentToken).Build()
|
|
||||||
resp, err := c.client.Drive.File.ListByIterator(ctx, req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("failed to list files")
|
|
||||||
return emptyFolderToken, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var file *larkdrive.File
|
|
||||||
for {
|
|
||||||
found, file, err = resp.Next()
|
|
||||||
if !found {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("failed to get next file")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if *file.Name == name {
|
|
||||||
objTokenCache.Set(folderPath, *file.Token, exOpts)
|
|
||||||
return *file.Token, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emptyFolderToken, false
|
|
||||||
}
|
|
||||||
@@ -257,18 +257,10 @@ func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
|
|
||||||
func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *Local) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
var err error
|
var err error
|
||||||
if utils.SliceContains([]string{"", "delete permanently"}, d.RecycleBinPath) {
|
if obj.IsDir() {
|
||||||
if obj.IsDir() {
|
err = os.RemoveAll(obj.GetPath())
|
||||||
err = os.RemoveAll(obj.GetPath())
|
|
||||||
} else {
|
|
||||||
err = os.Remove(obj.GetPath())
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
dstPath := filepath.Join(d.RecycleBinPath, obj.GetName())
|
err = os.Remove(obj.GetPath())
|
||||||
if utils.Exists(dstPath) {
|
|
||||||
dstPath = filepath.Join(d.RecycleBinPath, obj.GetName()+"_"+time.Now().Format("20060102150405"))
|
|
||||||
}
|
|
||||||
err = os.Rename(obj.GetPath(), dstPath)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ type Addition struct {
|
|||||||
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
ThumbCacheFolder string `json:"thumb_cache_folder"`
|
||||||
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
|
||||||
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
MkdirPerm string `json:"mkdir_perm" default:"777"`
|
||||||
RecycleBinPath string `json:"recycle_bin_path" default:"delete permanently" help:"path to recycle bin, delete permanently if empty or keep 'delete permanently'"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -188,9 +188,6 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
|||||||
_ = tempFile.Close()
|
_ = tempFile.Close()
|
||||||
}()
|
}()
|
||||||
uploader := s3manager.NewUploader(s)
|
uploader := s3manager.NewUploader(s)
|
||||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
|
||||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
|
||||||
}
|
|
||||||
input := &s3manager.UploadInput{
|
input := &s3manager.UploadInput{
|
||||||
Bucket: &resp.Data.Bucket,
|
Bucket: &resp.Data.Bucket,
|
||||||
Key: &resp.Data.Object,
|
Key: &resp.Data.Object,
|
||||||
@@ -206,7 +203,7 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h := md5.New()
|
h := md5.New()
|
||||||
_, err = utils.CopyWithBuffer(h, tempFile)
|
_, err = io.Copy(h, tempFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
"github.com/alist-org/alist/v3/pkg/http_range"
|
||||||
"github.com/pquerna/otp/totp"
|
|
||||||
"github.com/rclone/rclone/lib/readers"
|
"github.com/rclone/rclone/lib/readers"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
@@ -34,16 +33,8 @@ func (d *Mega) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Mega) Init(ctx context.Context) error {
|
func (d *Mega) Init(ctx context.Context) error {
|
||||||
var twoFACode = d.TwoFACode
|
|
||||||
d.c = mega.New()
|
d.c = mega.New()
|
||||||
if d.TwoFASecret != "" {
|
return d.c.Login(d.Email, d.Password)
|
||||||
code, err := totp.GenerateCode(d.TwoFASecret, time.Now())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("generate totp code failed: %w", err)
|
|
||||||
}
|
|
||||||
twoFACode = code
|
|
||||||
}
|
|
||||||
return d.c.MultiFactorLogin(d.Email, d.Password, twoFACode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Mega) Drop(ctx context.Context) error {
|
func (d *Mega) Drop(ctx context.Context) error {
|
||||||
|
|||||||
@@ -9,10 +9,8 @@ type Addition struct {
|
|||||||
// Usually one of two
|
// Usually one of two
|
||||||
//driver.RootPath
|
//driver.RootPath
|
||||||
//driver.RootID
|
//driver.RootID
|
||||||
Email string `json:"email" required:"true"`
|
Email string `json:"email" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Password string `json:"password" required:"true"`
|
||||||
TwoFACode string `json:"two_fa_code" required:"false" help:"2FA 6-digit code, filling in the 2FA code alone will not support reloading driver"`
|
|
||||||
TwoFASecret string `json:"two_fa_secret" required:"false" help:"2FA secret"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -43,31 +43,23 @@ func (d *MoPan) Init(ctx context.Context) error {
|
|||||||
if d.uploadThread < 1 || d.uploadThread > 32 {
|
if d.uploadThread < 1 || d.uploadThread > 32 {
|
||||||
d.uploadThread, d.UploadThread = 3, "3"
|
d.uploadThread, d.UploadThread = 3, "3"
|
||||||
}
|
}
|
||||||
|
login := func() error {
|
||||||
defer func() { d.SMSCode = "" }()
|
data, err := d.client.Login(d.Phone, d.Password)
|
||||||
|
|
||||||
login := func() (err error) {
|
|
||||||
var loginData *mopan.LoginResp
|
|
||||||
if d.SMSCode != "" {
|
|
||||||
loginData, err = d.client.LoginBySmsStep2(d.Phone, d.SMSCode)
|
|
||||||
} else {
|
|
||||||
loginData, err = d.client.Login(d.Phone, d.Password)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.client.SetAuthorization(loginData.Token)
|
d.client.SetAuthorization(data.Token)
|
||||||
|
|
||||||
info, err := d.client.GetUserInfo()
|
info, err := d.client.GetUserInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.userID = info.UserID
|
d.userID = info.UserID
|
||||||
log.Debugf("[mopan] Phone: %s UserCloudStorageRelations: %+v", d.Phone, loginData.UserCloudStorageRelations)
|
log.Debugf("[mopan] Phone: %s UserCloudStorageRelations: %+v", d.Phone, data.UserCloudStorageRelations)
|
||||||
cloudCircleApp, _ := d.client.QueryAllCloudCircleApp()
|
cloudCircleApp, _ := d.client.QueryAllCloudCircleApp()
|
||||||
log.Debugf("[mopan] Phone: %s CloudCircleApp: %+v", d.Phone, cloudCircleApp)
|
log.Debugf("[mopan] Phone: %s CloudCircleApp: %+v", d.Phone, cloudCircleApp)
|
||||||
if d.RootFolderID == "" {
|
if d.RootFolderID == "" {
|
||||||
for _, userCloudStorage := range loginData.UserCloudStorageRelations {
|
for _, userCloudStorage := range data.UserCloudStorageRelations {
|
||||||
if userCloudStorage.Path == "/文件" {
|
if userCloudStorage.Path == "/文件" {
|
||||||
d.RootFolderID = userCloudStorage.FolderID
|
d.RootFolderID = userCloudStorage.FolderID
|
||||||
}
|
}
|
||||||
@@ -84,20 +76,8 @@ func (d *MoPan) Init(ctx context.Context) error {
|
|||||||
op.MustSaveDriverStorage(d)
|
op.MustSaveDriverStorage(d)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
})
|
}).SetDeviceInfo(d.DeviceInfo)
|
||||||
|
d.DeviceInfo = d.client.GetDeviceInfo()
|
||||||
var deviceInfo mopan.DeviceInfo
|
|
||||||
if strings.TrimSpace(d.DeviceInfo) != "" && utils.Json.UnmarshalFromString(d.DeviceInfo, &deviceInfo) == nil {
|
|
||||||
d.client.SetDeviceInfo(&deviceInfo)
|
|
||||||
}
|
|
||||||
d.DeviceInfo, _ = utils.Json.MarshalToString(d.client.GetDeviceInfo())
|
|
||||||
|
|
||||||
if strings.Contains(d.SMSCode, "send") {
|
|
||||||
if _, err := d.client.LoginBySms(d.Phone); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return errors.New("please enter the SMS code")
|
|
||||||
}
|
|
||||||
return login()
|
return login()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +275,7 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !initUpdload.FileDataExists {
|
if !initUpdload.FileDataExists {
|
||||||
// utils.Log.Error(d.client.CloudDiskStartBusiness())
|
utils.Log.Error(d.client.CloudDiskStartBusiness())
|
||||||
|
|
||||||
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
threadG, upCtx := errgroup.NewGroupWithContext(ctx, d.uploadThread,
|
||||||
retry.Attempts(3),
|
retry.Attempts(3),
|
||||||
@@ -323,7 +303,6 @@ func (d *MoPan) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.ContentLength = byteSize
|
|
||||||
resp, err := base.HttpClient.Do(req)
|
resp, err := base.HttpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
Phone string `json:"phone" required:"true"`
|
Phone string `json:"phone" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Password string `json:"password" required:"true"`
|
||||||
SMSCode string `json:"sms_code" help:"input 'send' send sms "`
|
|
||||||
|
|
||||||
RootFolderID string `json:"root_folder_id" default:""`
|
RootFolderID string `json:"root_folder_id" default:""`
|
||||||
|
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
linuxapiKey = []byte("rFgB&h#%2?^eDg:Q")
|
|
||||||
eapiKey = []byte("e82ckenh8dichen8")
|
|
||||||
iv = []byte("0102030405060708")
|
|
||||||
presetKey = []byte("0CoJUm6Qyw8W8jud")
|
|
||||||
publicKey = []byte("-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB\n-----END PUBLIC KEY-----")
|
|
||||||
stdChars = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
|
||||||
)
|
|
||||||
|
|
||||||
func aesKeyPending(key []byte) []byte {
|
|
||||||
k := len(key)
|
|
||||||
count := 0
|
|
||||||
switch true {
|
|
||||||
case k <= 16:
|
|
||||||
count = 16 - k
|
|
||||||
case k <= 24:
|
|
||||||
count = 24 - k
|
|
||||||
case k <= 32:
|
|
||||||
count = 32 - k
|
|
||||||
default:
|
|
||||||
return key[:32]
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(key, bytes.Repeat([]byte{0}, count)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(src)%blockSize
|
|
||||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(src, padtext...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func aesCBCEncrypt(src, key, iv []byte) []byte {
|
|
||||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
|
||||||
src = pkcs7Padding(src, block.BlockSize())
|
|
||||||
dst := make([]byte, len(src))
|
|
||||||
|
|
||||||
mode := cipher.NewCBCEncrypter(block, iv)
|
|
||||||
mode.CryptBlocks(dst, src)
|
|
||||||
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func aesECBEncrypt(src, key []byte) []byte {
|
|
||||||
block, _ := aes.NewCipher(aesKeyPending(key))
|
|
||||||
|
|
||||||
src = pkcs7Padding(src, block.BlockSize())
|
|
||||||
dst := make([]byte, len(src))
|
|
||||||
|
|
||||||
ecbCryptBlocks(block, dst, src)
|
|
||||||
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func ecbCryptBlocks(block cipher.Block, dst, src []byte) {
|
|
||||||
bs := block.BlockSize()
|
|
||||||
|
|
||||||
for len(src) > 0 {
|
|
||||||
block.Encrypt(dst, src[:bs])
|
|
||||||
src = src[bs:]
|
|
||||||
dst = dst[bs:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func rsaEncrypt(buffer, key []byte) []byte {
|
|
||||||
buffers := make([]byte, 128-16, 128)
|
|
||||||
buffers = append(buffers, buffer...)
|
|
||||||
block, _ := pem.Decode(key)
|
|
||||||
pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
|
|
||||||
pub := pubInterface.(*rsa.PublicKey)
|
|
||||||
c := new(big.Int).SetBytes([]byte(buffers))
|
|
||||||
return c.Exp(c, big.NewInt(int64(pub.E)), pub.N).Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSecretKey() ([]byte, []byte) {
|
|
||||||
key := make([]byte, 16)
|
|
||||||
reversed := make([]byte, 16)
|
|
||||||
for i := 0; i < 16; i++ {
|
|
||||||
result := stdChars[random.RangeInt64(0, 62)]
|
|
||||||
key[i] = result
|
|
||||||
reversed[15-i] = result
|
|
||||||
}
|
|
||||||
return key, reversed
|
|
||||||
}
|
|
||||||
|
|
||||||
func weapi(data map[string]string) map[string]string {
|
|
||||||
text, _ := utils.Json.Marshal(data)
|
|
||||||
secretKey, reversedKey := getSecretKey()
|
|
||||||
params := []byte(base64.StdEncoding.EncodeToString(aesCBCEncrypt(text, presetKey, iv)))
|
|
||||||
return map[string]string{
|
|
||||||
"params": base64.StdEncoding.EncodeToString(aesCBCEncrypt(params, reversedKey, iv)),
|
|
||||||
"encSecKey": hex.EncodeToString(rsaEncrypt(secretKey, publicKey)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func eapi(url string, data map[string]interface{}) map[string]string {
|
|
||||||
text, _ := utils.Json.Marshal(data)
|
|
||||||
msg := "nobody" + url + "use" + string(text) + "md5forencrypt"
|
|
||||||
h := md5.New()
|
|
||||||
h.Write([]byte(msg))
|
|
||||||
digest := hex.EncodeToString(h.Sum(nil))
|
|
||||||
params := []byte(url + "-36cd479b6b5-" + string(text) + "-36cd479b6b5-" + digest)
|
|
||||||
return map[string]string{
|
|
||||||
"params": hex.EncodeToString(aesECBEncrypt(params, eapiKey)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func linuxapi(data map[string]interface{}) map[string]string {
|
|
||||||
text, _ := utils.Json.Marshal(data)
|
|
||||||
return map[string]string{
|
|
||||||
"eparams": strings.ToUpper(hex.EncodeToString(aesECBEncrypt(text, linuxapiKey))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
)
|
|
||||||
|
|
||||||
type NeteaseMusic struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
|
|
||||||
csrfToken string
|
|
||||||
musicU string
|
|
||||||
fileMapByName map[string]model.Obj
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Init(ctx context.Context) error {
|
|
||||||
d.csrfToken = d.Addition.getCookie("__csrf")
|
|
||||||
d.musicU = d.Addition.getCookie("MUSIC_U")
|
|
||||||
|
|
||||||
if d.csrfToken == "" || d.musicU == "" {
|
|
||||||
return errs.EmptyToken
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Get(ctx context.Context, path string) (model.Obj, error) {
|
|
||||||
if path == "/" {
|
|
||||||
return &model.Object{
|
|
||||||
IsFolder: true,
|
|
||||||
Path: path,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
fragments := strings.Split(path, "/")
|
|
||||||
if len(fragments) > 1 {
|
|
||||||
fileName := fragments[1]
|
|
||||||
if strings.HasSuffix(fileName, ".lrc") {
|
|
||||||
lrc := d.fileMapByName[fileName]
|
|
||||||
return d.getLyricObj(lrc)
|
|
||||||
}
|
|
||||||
if song, ok := d.fileMapByName[fileName]; ok {
|
|
||||||
return song, nil
|
|
||||||
} else {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
return d.getSongObjs(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if lrc, ok := file.(*LyricObj); ok {
|
|
||||||
if args.Type == "parsed" {
|
|
||||||
return lrc.getLyricLink(), nil
|
|
||||||
} else {
|
|
||||||
return lrc.getProxyLink(args), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.getSongLink(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
return d.removeSongObj(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
|
||||||
return d.putSongStream(stream)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
|
||||||
return errs.NotSupport
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*NeteaseMusic)(nil)
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
Cookie string `json:"cookie" type:"text" required:"true" help:""`
|
|
||||||
SongLimit uint64 `json:"song_limit" default:"200" type:"number" help:"only get 200 songs by default"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ad *Addition) getCookie(name string) string {
|
|
||||||
re := regexp.MustCompile(name + "=([^(;|$)]+)")
|
|
||||||
matches := re.FindStringSubmatch(ad.Cookie)
|
|
||||||
if len(matches) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return matches[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "NeteaseMusic",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &NeteaseMusic{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/sign"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils/random"
|
|
||||||
"github.com/alist-org/alist/v3/server/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HostsResp struct {
|
|
||||||
Upload []string `json:"upload"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SongResp struct {
|
|
||||||
Data []struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListResp struct {
|
|
||||||
Size string `json:"size"`
|
|
||||||
MaxSize string `json:"maxSize"`
|
|
||||||
Data []struct {
|
|
||||||
AddTime int64 `json:"addTime"`
|
|
||||||
FileName string `json:"fileName"`
|
|
||||||
FileSize int64 `json:"fileSize"`
|
|
||||||
SongId int64 `json:"songId"`
|
|
||||||
SimpleSong struct {
|
|
||||||
Al struct {
|
|
||||||
PicUrl string `json:"picUrl"`
|
|
||||||
} `json:"al"`
|
|
||||||
} `json:"simpleSong"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LyricObj struct {
|
|
||||||
model.Object
|
|
||||||
lyric string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lrc *LyricObj) getProxyLink(args model.LinkArgs) *model.Link {
|
|
||||||
rawURL := common.GetApiUrl(args.HttpReq) + "/p" + lrc.Path
|
|
||||||
rawURL = utils.EncodePath(rawURL, true) + "?type=parsed&sign=" + sign.Sign(lrc.Path)
|
|
||||||
return &model.Link{URL: rawURL}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lrc *LyricObj) getLyricLink() *model.Link {
|
|
||||||
reader := strings.NewReader(lrc.lyric)
|
|
||||||
return &model.Link{
|
|
||||||
RangeReadCloser: &model.RangeReadCloser{
|
|
||||||
RangeReader: func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
|
||||||
if httpRange.Length < 0 {
|
|
||||||
return io.NopCloser(reader), nil
|
|
||||||
}
|
|
||||||
sr := io.NewSectionReader(reader, httpRange.Start, httpRange.Length)
|
|
||||||
return io.NopCloser(sr), nil
|
|
||||||
},
|
|
||||||
Closers: utils.EmptyClosers(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReqOption struct {
|
|
||||||
crypto string
|
|
||||||
stream model.FileStreamer
|
|
||||||
data map[string]string
|
|
||||||
headers map[string]string
|
|
||||||
cookies []*http.Cookie
|
|
||||||
url string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Characteristic map[string]string
|
|
||||||
|
|
||||||
func (ch *Characteristic) fromDriver(d *NeteaseMusic) *Characteristic {
|
|
||||||
*ch = map[string]string{
|
|
||||||
"osver": "",
|
|
||||||
"deviceId": "",
|
|
||||||
"mobilename": "",
|
|
||||||
"appver": "6.1.1",
|
|
||||||
"versioncode": "140",
|
|
||||||
"buildver": strconv.FormatInt(time.Now().Unix(), 10),
|
|
||||||
"resolution": "1920x1080",
|
|
||||||
"os": "android",
|
|
||||||
"channel": "",
|
|
||||||
"requestId": strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(int(random.RangeInt64(0, 1000))),
|
|
||||||
"MUSIC_U": d.musicU,
|
|
||||||
}
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch Characteristic) toCookies() []*http.Cookie {
|
|
||||||
cookies := make([]*http.Cookie, 0)
|
|
||||||
for k, v := range ch {
|
|
||||||
cookies = append(cookies, &http.Cookie{Name: k, Value: v})
|
|
||||||
}
|
|
||||||
return cookies
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ch *Characteristic) merge(data map[string]string) map[string]interface{} {
|
|
||||||
body := map[string]interface{}{
|
|
||||||
"header": ch,
|
|
||||||
}
|
|
||||||
for k, v := range data {
|
|
||||||
body[k] = v
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/dhowden/tag"
|
|
||||||
)
|
|
||||||
|
|
||||||
type token struct {
|
|
||||||
resourceId string
|
|
||||||
objectKey string
|
|
||||||
token string
|
|
||||||
}
|
|
||||||
|
|
||||||
type songmeta struct {
|
|
||||||
needUpload bool
|
|
||||||
songId string
|
|
||||||
name string
|
|
||||||
artist string
|
|
||||||
album string
|
|
||||||
}
|
|
||||||
|
|
||||||
type uploader struct {
|
|
||||||
driver *NeteaseMusic
|
|
||||||
file model.File
|
|
||||||
meta songmeta
|
|
||||||
md5 string
|
|
||||||
ext string
|
|
||||||
size string
|
|
||||||
filename string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uploader) init(stream model.FileStreamer) error {
|
|
||||||
u.filename = stream.GetName()
|
|
||||||
u.size = strconv.FormatInt(stream.GetSize(), 10)
|
|
||||||
|
|
||||||
u.ext = "mp3"
|
|
||||||
if strings.HasSuffix(stream.GetMimetype(), "flac") {
|
|
||||||
u.ext = "flac"
|
|
||||||
}
|
|
||||||
|
|
||||||
h := md5.New()
|
|
||||||
io.Copy(h, stream)
|
|
||||||
u.md5 = hex.EncodeToString(h.Sum(nil))
|
|
||||||
_, err := u.file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if m, err := tag.ReadFrom(u.file); err != nil {
|
|
||||||
u.meta = songmeta{}
|
|
||||||
} else {
|
|
||||||
u.meta = songmeta{
|
|
||||||
name: m.Title(),
|
|
||||||
artist: m.Artist(),
|
|
||||||
album: m.Album(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if u.meta.name == "" {
|
|
||||||
u.meta.name = u.filename
|
|
||||||
}
|
|
||||||
if u.meta.album == "" {
|
|
||||||
u.meta.album = "未知专辑"
|
|
||||||
}
|
|
||||||
if u.meta.artist == "" {
|
|
||||||
u.meta.artist = "未知艺术家"
|
|
||||||
}
|
|
||||||
_, err = u.file.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uploader) checkIfExisted() error {
|
|
||||||
body, err := u.driver.request("https://interface.music.163.com/api/cloud/upload/check", http.MethodPost,
|
|
||||||
ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"ext": "",
|
|
||||||
"songId": "0",
|
|
||||||
"version": "1",
|
|
||||||
"bitrate": "999000",
|
|
||||||
"length": u.size,
|
|
||||||
"md5": u.md5,
|
|
||||||
},
|
|
||||||
cookies: []*http.Cookie{
|
|
||||||
{Name: "os", Value: "pc"},
|
|
||||||
{Name: "appver", Value: "2.9.7"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
u.meta.songId = utils.Json.Get(body, "songId").ToString()
|
|
||||||
u.meta.needUpload = utils.Json.Get(body, "needUpload").ToBool()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uploader) allocToken(bucket ...string) (token, error) {
|
|
||||||
if len(bucket) == 0 {
|
|
||||||
bucket = []string{""}
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := u.driver.request("https://music.163.com/weapi/nos/token/alloc", http.MethodPost, ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"bucket": bucket[0],
|
|
||||||
"local": "false",
|
|
||||||
"type": "audio",
|
|
||||||
"nos_product": "3",
|
|
||||||
"filename": u.filename,
|
|
||||||
"md5": u.md5,
|
|
||||||
"ext": u.ext,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return token{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return token{
|
|
||||||
resourceId: utils.Json.Get(body, "result", "resourceId").ToString(),
|
|
||||||
objectKey: utils.Json.Get(body, "result", "objectKey").ToString(),
|
|
||||||
token: utils.Json.Get(body, "result", "token").ToString(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uploader) publishInfo(resourceId string) error {
|
|
||||||
body, err := u.driver.request("https://music.163.com/api/upload/cloud/info/v2", http.MethodPost, ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"md5": u.md5,
|
|
||||||
"filename": u.filename,
|
|
||||||
"song": u.meta.name,
|
|
||||||
"album": u.meta.album,
|
|
||||||
"artist": u.meta.artist,
|
|
||||||
"songid": u.meta.songId,
|
|
||||||
"resourceId": resourceId,
|
|
||||||
"bitrate": "999000",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = u.driver.request("https://interface.music.163.com/api/cloud/pub/v2", http.MethodPost, ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"songid": utils.Json.Get(body, "songId").ToString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *uploader) upload(stream model.FileStreamer) error {
|
|
||||||
bucket := "jd-musicrep-privatecloud-audio-public"
|
|
||||||
token, err := u.allocToken(bucket)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := u.driver.request("https://wanproxy.127.net/lbs?version=1.0&bucketname="+bucket, http.MethodGet,
|
|
||||||
ReqOption{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var resp HostsResp
|
|
||||||
err = utils.Json.Unmarshal(body, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
objectKey := strings.ReplaceAll(token.objectKey, "/", "%2F")
|
|
||||||
_, err = u.driver.request(
|
|
||||||
resp.Upload[0]+"/"+bucket+"/"+objectKey+"?offset=0&complete=true&version=1.0",
|
|
||||||
http.MethodPost,
|
|
||||||
ReqOption{
|
|
||||||
stream: stream,
|
|
||||||
headers: map[string]string{
|
|
||||||
"x-nos-token": token.token,
|
|
||||||
"Content-Type": "audio/mpeg",
|
|
||||||
"Content-Length": u.size,
|
|
||||||
"Content-MD5": u.md5,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
package netease_music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) request(url, method string, opt ReqOption) ([]byte, error) {
|
|
||||||
req := base.RestyClient.R()
|
|
||||||
|
|
||||||
req.SetHeader("Cookie", d.Addition.Cookie)
|
|
||||||
|
|
||||||
if strings.Contains(url, "music.163.com") {
|
|
||||||
req.SetHeader("Referer", "https://music.163.com")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.cookies != nil {
|
|
||||||
for _, cookie := range opt.cookies {
|
|
||||||
req.SetCookie(cookie)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.headers != nil {
|
|
||||||
for header, value := range opt.headers {
|
|
||||||
req.SetHeader(header, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data := opt.data
|
|
||||||
if opt.crypto == "weapi" {
|
|
||||||
data = weapi(data)
|
|
||||||
re, _ := regexp.Compile(`/\w*api/`)
|
|
||||||
url = re.ReplaceAllString(url, "/weapi/")
|
|
||||||
} else if opt.crypto == "eapi" {
|
|
||||||
ch := new(Characteristic).fromDriver(d)
|
|
||||||
req.SetCookies(ch.toCookies())
|
|
||||||
data = eapi(opt.url, ch.merge(data))
|
|
||||||
re, _ := regexp.Compile(`/\w*api/`)
|
|
||||||
url = re.ReplaceAllString(url, "/eapi/")
|
|
||||||
} else if opt.crypto == "linuxapi" {
|
|
||||||
re, _ := regexp.Compile(`/\w*api/`)
|
|
||||||
data = linuxapi(map[string]interface{}{
|
|
||||||
"url": re.ReplaceAllString(url, "/api/"),
|
|
||||||
"method": method,
|
|
||||||
"params": data,
|
|
||||||
})
|
|
||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")
|
|
||||||
url = "https://music.163.com/api/linux/forward"
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == http.MethodPost {
|
|
||||||
if opt.stream != nil {
|
|
||||||
req.SetContentLength(true)
|
|
||||||
req.SetBody(io.ReadCloser(opt.stream))
|
|
||||||
} else {
|
|
||||||
req.SetFormData(data)
|
|
||||||
}
|
|
||||||
res, err := req.Post(url)
|
|
||||||
return res.Body(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
if method == http.MethodGet {
|
|
||||||
res, err := req.Get(url)
|
|
||||||
return res.Body(), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errs.NotImplement
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) getSongObjs(args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
body, err := d.request("https://music.163.com/weapi/v1/cloud/get", http.MethodPost, ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"limit": strconv.FormatUint(d.Addition.SongLimit, 10),
|
|
||||||
"offset": "0",
|
|
||||||
},
|
|
||||||
cookies: []*http.Cookie{
|
|
||||||
{Name: "os", Value: "pc"},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp ListResp
|
|
||||||
err = utils.Json.Unmarshal(body, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
d.fileMapByName = make(map[string]model.Obj)
|
|
||||||
files := make([]model.Obj, 0, len(resp.Data))
|
|
||||||
for _, f := range resp.Data {
|
|
||||||
song := &model.ObjThumb{
|
|
||||||
Object: model.Object{
|
|
||||||
IsFolder: false,
|
|
||||||
Size: f.FileSize,
|
|
||||||
Name: f.FileName,
|
|
||||||
Modified: time.UnixMilli(f.AddTime),
|
|
||||||
ID: strconv.FormatInt(f.SongId, 10),
|
|
||||||
},
|
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: f.SimpleSong.Al.PicUrl},
|
|
||||||
}
|
|
||||||
d.fileMapByName[song.Name] = song
|
|
||||||
files = append(files, song)
|
|
||||||
|
|
||||||
// map song id for lyric
|
|
||||||
lrcName := strings.Split(f.FileName, ".")[0] + ".lrc"
|
|
||||||
lrc := &model.Object{
|
|
||||||
IsFolder: false,
|
|
||||||
Name: lrcName,
|
|
||||||
Path: path.Join(args.ReqPath, lrcName),
|
|
||||||
ID: strconv.FormatInt(f.SongId, 10),
|
|
||||||
}
|
|
||||||
d.fileMapByName[lrc.Name] = lrc
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) getSongLink(file model.Obj) (*model.Link, error) {
|
|
||||||
body, err := d.request(
|
|
||||||
"https://music.163.com/api/song/enhance/player/url", http.MethodPost, ReqOption{
|
|
||||||
crypto: "linuxapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"ids": "[" + file.GetID() + "]",
|
|
||||||
"br": "999000",
|
|
||||||
},
|
|
||||||
cookies: []*http.Cookie{
|
|
||||||
{Name: "os", Value: "pc"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var resp SongResp
|
|
||||||
err = utils.Json.Unmarshal(body, &resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Data) < 1 {
|
|
||||||
return nil, errs.ObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Link{URL: resp.Data[0].Url}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) getLyricObj(file model.Obj) (model.Obj, error) {
|
|
||||||
if lrc, ok := file.(*LyricObj); ok {
|
|
||||||
return lrc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := d.request(
|
|
||||||
"https://music.163.com/api/song/lyric?_nmclfl=1", http.MethodPost, ReqOption{
|
|
||||||
data: map[string]string{
|
|
||||||
"id": file.GetID(),
|
|
||||||
"tv": "-1",
|
|
||||||
"lv": "-1",
|
|
||||||
"rv": "-1",
|
|
||||||
"kv": "-1",
|
|
||||||
},
|
|
||||||
cookies: []*http.Cookie{
|
|
||||||
{Name: "os", Value: "ios"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
lyric := utils.Json.Get(body, "lrc", "lyric").ToString()
|
|
||||||
|
|
||||||
return &LyricObj{
|
|
||||||
lyric: lyric,
|
|
||||||
Object: model.Object{
|
|
||||||
IsFolder: false,
|
|
||||||
ID: file.GetID(),
|
|
||||||
Name: file.GetName(),
|
|
||||||
Path: file.GetPath(),
|
|
||||||
Size: int64(len(lyric)),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) removeSongObj(file model.Obj) error {
|
|
||||||
_, err := d.request("http://music.163.com/weapi/cloud/del", http.MethodPost, ReqOption{
|
|
||||||
crypto: "weapi",
|
|
||||||
data: map[string]string{
|
|
||||||
"songIds": "[" + file.GetID() + "]",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *NeteaseMusic) putSongStream(stream model.FileStreamer) error {
|
|
||||||
tmp, err := stream.CacheFullInTempFile()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer tmp.Close()
|
|
||||||
|
|
||||||
u := uploader{driver: d, file: tmp}
|
|
||||||
|
|
||||||
err = u.init(stream)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = u.checkIfExisted()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
token, err := u.allocToken()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.meta.needUpload {
|
|
||||||
err = u.upload(stream)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = u.publishInfo(token.resourceId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -118,7 +118,6 @@ func (d *Onedrive) MakeDir(ctx context.Context, parentDir model.Obj, dirName str
|
|||||||
"folder": base.Json{},
|
"folder": base.Json{},
|
||||||
"@microsoft.graph.conflictBehavior": "rename",
|
"@microsoft.graph.conflictBehavior": "rename",
|
||||||
}
|
}
|
||||||
// todo 修复文件夹 ctime/mtime, onedrive 可在 data 里设置 fileSystemInfo 字段, 但是此接口未提供 ctime/mtime
|
|
||||||
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
_, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
||||||
req.SetBody(data)
|
req.SetBody(data)
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ type RespErr struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
FileSystemInfo *FileSystemInfoFacet `json:"fileSystemInfo"`
|
LastModifiedDateTime time.Time `json:"lastModifiedDateTime"`
|
||||||
Url string `json:"@microsoft.graph.downloadUrl"`
|
Url string `json:"@microsoft.graph.downloadUrl"`
|
||||||
File *struct {
|
File *struct {
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
} `json:"file"`
|
} `json:"file"`
|
||||||
Thumbnails []struct {
|
Thumbnails []struct {
|
||||||
@@ -58,7 +58,7 @@ func fileToObj(f File, parentID string) *Object {
|
|||||||
ID: f.Id,
|
ID: f.Id,
|
||||||
Name: f.Name,
|
Name: f.Name,
|
||||||
Size: f.Size,
|
Size: f.Size,
|
||||||
Modified: f.FileSystemInfo.LastModifiedDateTime,
|
Modified: f.LastModifiedDateTime,
|
||||||
IsFolder: f.File == nil,
|
IsFolder: f.File == nil,
|
||||||
},
|
},
|
||||||
Thumbnail: model.Thumbnail{Thumbnail: thumb},
|
Thumbnail: model.Thumbnail{Thumbnail: thumb},
|
||||||
@@ -72,20 +72,3 @@ type Files struct {
|
|||||||
Value []File `json:"value"`
|
Value []File `json:"value"`
|
||||||
NextLink string `json:"@odata.nextLink"`
|
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.
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ func (d *Onedrive) Request(url string, method string, callback base.ReqCallback,
|
|||||||
|
|
||||||
func (d *Onedrive) getFiles(path string) ([]File, error) {
|
func (d *Onedrive) getFiles(path string) ([]File, error) {
|
||||||
var res []File
|
var res []File
|
||||||
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,fileSystemInfo,content.downloadUrl,file,parentReference"
|
nextLink := d.GetMetaUrl(false, path) + "/children?$top=5000&$expand=thumbnails($select=medium)&$select=id,name,size,lastModifiedDateTime,content.downloadUrl,file,parentReference"
|
||||||
for nextLink != "" {
|
for nextLink != "" {
|
||||||
var files Files
|
var files Files
|
||||||
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
_, err := d.Request(nextLink, http.MethodGet, nil, &files)
|
||||||
@@ -148,10 +148,7 @@ func (d *Onedrive) GetFile(path string) (*File, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
|
func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.FileStreamer) error {
|
||||||
filepath := stdpath.Join(dstDir.GetPath(), stream.GetName())
|
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/content"
|
||||||
// 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)
|
data, err := io.ReadAll(stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -159,50 +156,12 @@ func (d *Onedrive) upSmall(ctx context.Context, dstDir model.Obj, stream model.F
|
|||||||
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
|
_, err = d.Request(url, http.MethodPut, func(req *resty.Request) {
|
||||||
req.SetBody(data).SetContext(ctx)
|
req.SetBody(data).SetContext(ctx)
|
||||||
}, nil)
|
}, 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
|
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 {
|
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"
|
url := d.GetMetaUrl(false, stdpath.Join(dstDir.GetPath(), stream.GetName())) + "/createUploadSession"
|
||||||
metadata := map[string]interface{}{"item": toAPIMetadata(stream)}
|
res, err := d.Request(url, http.MethodPost, nil, nil)
|
||||||
res, err := d.Request(url, http.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetBody(metadata).SetContext(ctx)
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,13 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PikPak struct {
|
type PikPak struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
RefreshToken string
|
||||||
oauth2Token oauth2.TokenSource
|
AccessToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PikPak) Config() driver.Config {
|
func (d *PikPak) Config() driver.Config {
|
||||||
@@ -35,32 +34,8 @@ func (d *PikPak) GetAddition() driver.Additional {
|
|||||||
return &d.Addition
|
return &d.Addition
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PikPak) Init(ctx context.Context) (err error) {
|
func (d *PikPak) Init(ctx context.Context) error {
|
||||||
if d.ClientID == "" || d.ClientSecret == "" {
|
return d.login()
|
||||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
|
||||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
|
||||||
}
|
|
||||||
|
|
||||||
withClient := func(ctx context.Context) context.Context {
|
|
||||||
return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Config := &oauth2.Config{
|
|
||||||
ClientID: d.ClientID,
|
|
||||||
ClientSecret: d.ClientSecret,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
|
||||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
|
||||||
AuthStyle: oauth2.AuthStyleInParams,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PikPak) Drop(ctx context.Context) error {
|
func (d *PikPak) Drop(ctx context.Context) error {
|
||||||
@@ -197,9 +172,6 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
uploader := s3manager.NewUploader(ss)
|
uploader := s3manager.NewUploader(ss)
|
||||||
if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
|
|
||||||
uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
|
|
||||||
}
|
|
||||||
input := &s3manager.UploadInput{
|
input := &s3manager.UploadInput{
|
||||||
Bucket: ¶ms.Bucket,
|
Bucket: ¶ms.Bucket,
|
||||||
Key: ¶ms.Key,
|
Key: ¶ms.Key,
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ type Addition struct {
|
|||||||
driver.RootID
|
driver.RootID
|
||||||
Username string `json:"username" required:"true"`
|
Username string `json:"username" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Password string `json:"password" required:"true"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
|
||||||
DisableMediaLink bool `json:"disable_media_link"`
|
DisableMediaLink bool `json:"disable_media_link"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,77 @@
|
|||||||
package pikpak
|
package pikpak
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
|
func (d *PikPak) login() error {
|
||||||
|
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||||
|
var e RespErr
|
||||||
|
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||||
|
"captcha_token": "",
|
||||||
|
"client_id": "YNxT9w7GMdWvEOKa",
|
||||||
|
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||||
|
"username": d.Username,
|
||||||
|
"password": d.Password,
|
||||||
|
}).Post(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.ErrorCode != 0 {
|
||||||
|
return errors.New(e.Error)
|
||||||
|
}
|
||||||
|
data := res.Body()
|
||||||
|
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||||
|
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PikPak) refreshToken() error {
|
||||||
|
url := "https://user.mypikpak.com/v1/auth/token"
|
||||||
|
var e RespErr
|
||||||
|
res, err := base.RestyClient.R().SetError(&e).
|
||||||
|
SetHeader("user-agent", "").SetBody(base.Json{
|
||||||
|
"client_id": "YNxT9w7GMdWvEOKa",
|
||||||
|
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": d.RefreshToken,
|
||||||
|
}).Post(url)
|
||||||
|
if err != nil {
|
||||||
|
d.Status = err.Error()
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.ErrorCode != 0 {
|
||||||
|
if e.ErrorCode == 4126 {
|
||||||
|
// refresh_token invalid, re-login
|
||||||
|
return d.login()
|
||||||
|
}
|
||||||
|
d.Status = e.Error
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return errors.New(e.Error)
|
||||||
|
}
|
||||||
|
data := res.Body()
|
||||||
|
d.Status = "work"
|
||||||
|
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||||
|
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
token, err := d.oauth2Token.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
}
|
}
|
||||||
@@ -31,9 +84,17 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.ErrorCode != 0 {
|
if e.ErrorCode != 0 {
|
||||||
return nil, errors.New(e.Error)
|
if e.ErrorCode == 16 {
|
||||||
|
// login / refresh token
|
||||||
|
err = d.refreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.request(url, method, callback, resp)
|
||||||
|
} else {
|
||||||
|
return nil, errors.New(e.Error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return res.Body(), nil
|
return res.Body(), nil
|
||||||
}
|
}
|
||||||
@@ -65,3 +126,28 @@ func (d *PikPak) getFiles(id string) ([]File, error) {
|
|||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getGcid(r io.Reader, size int64) (string, error) {
|
||||||
|
calcBlockSize := func(j int64) int64 {
|
||||||
|
var psize int64 = 0x40000
|
||||||
|
for float64(j)/float64(psize) > 0x200 && psize < 0x200000 {
|
||||||
|
psize = psize << 1
|
||||||
|
}
|
||||||
|
return psize
|
||||||
|
}
|
||||||
|
|
||||||
|
hash1 := sha1.New()
|
||||||
|
hash2 := sha1.New()
|
||||||
|
readSize := calcBlockSize(size)
|
||||||
|
for {
|
||||||
|
hash2.Reset()
|
||||||
|
if n, err := io.CopyN(hash2, r, readSize); err != nil && n == 0 {
|
||||||
|
if err != io.EOF {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
hash1.Write(hash2.Sum(nil))
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hash1.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
"github.com/alist-org/alist/v3/internal/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
"github.com/alist-org/alist/v3/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PikPakShare struct {
|
type PikPakShare struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
oauth2Token oauth2.TokenSource
|
RefreshToken string
|
||||||
|
AccessToken string
|
||||||
PassCodeToken string
|
PassCodeToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,31 +27,10 @@ func (d *PikPakShare) GetAddition() driver.Additional {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *PikPakShare) Init(ctx context.Context) error {
|
func (d *PikPakShare) Init(ctx context.Context) error {
|
||||||
if d.ClientID == "" || d.ClientSecret == "" {
|
err := d.login()
|
||||||
d.ClientID = "YNxT9w7GMdWvEOKa"
|
|
||||||
d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
|
|
||||||
}
|
|
||||||
|
|
||||||
withClient := func(ctx context.Context) context.Context {
|
|
||||||
return context.WithValue(ctx, oauth2.HTTPClient, base.HttpClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Config := &oauth2.Config{
|
|
||||||
ClientID: d.ClientID,
|
|
||||||
ClientSecret: d.ClientSecret,
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: "https://user.mypikpak.com/v1/auth/signin",
|
|
||||||
TokenURL: "https://user.mypikpak.com/v1/auth/token",
|
|
||||||
AuthStyle: oauth2.AuthStyleInParams,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
oauth2Token, err := oauth2Config.PasswordCredentialsToken(withClient(ctx), d.Username, d.Password)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.oauth2Token = oauth2Config.TokenSource(withClient(context.Background()), oauth2Token)
|
|
||||||
|
|
||||||
if d.SharePwd != "" {
|
if d.SharePwd != "" {
|
||||||
err = d.getSharePassToken()
|
err = d.getSharePassToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -89,14 +67,8 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadUrl := resp.FileInfo.WebContentLink
|
|
||||||
if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 {
|
|
||||||
downloadUrl = resp.FileInfo.Medias[0].Link.Url
|
|
||||||
}
|
|
||||||
|
|
||||||
link := model.Link{
|
link := model.Link{
|
||||||
URL: downloadUrl,
|
URL: resp.FileInfo.WebContentLink,
|
||||||
}
|
}
|
||||||
return &link, nil
|
return &link, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import (
|
|||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootID
|
driver.RootID
|
||||||
Username string `json:"username" required:"true"`
|
Username string `json:"username" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Password string `json:"password" required:"true"`
|
||||||
ShareId string `json:"share_id" required:"true"`
|
ShareId string `json:"share_id" required:"true"`
|
||||||
SharePwd string `json:"share_pwd"`
|
SharePwd string `json:"share_pwd"`
|
||||||
ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"`
|
|
||||||
ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -5,18 +5,70 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
|
"github.com/alist-org/alist/v3/internal/op"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
|
func (d *PikPakShare) login() error {
|
||||||
|
url := "https://user.mypikpak.com/v1/auth/signin"
|
||||||
|
var e RespErr
|
||||||
|
res, err := base.RestyClient.R().SetError(&e).SetBody(base.Json{
|
||||||
|
"captcha_token": "",
|
||||||
|
"client_id": "YNxT9w7GMdWvEOKa",
|
||||||
|
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||||
|
"username": d.Username,
|
||||||
|
"password": d.Password,
|
||||||
|
}).Post(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.ErrorCode != 0 {
|
||||||
|
return errors.New(e.Error)
|
||||||
|
}
|
||||||
|
data := res.Body()
|
||||||
|
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||||
|
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *PikPakShare) refreshToken() error {
|
||||||
|
url := "https://user.mypikpak.com/v1/auth/token"
|
||||||
|
var e RespErr
|
||||||
|
res, err := base.RestyClient.R().SetError(&e).
|
||||||
|
SetHeader("user-agent", "").SetBody(base.Json{
|
||||||
|
"client_id": "YNxT9w7GMdWvEOKa",
|
||||||
|
"client_secret": "dbw2OtmVEeuUvIptb1Coyg",
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": d.RefreshToken,
|
||||||
|
}).Post(url)
|
||||||
|
if err != nil {
|
||||||
|
d.Status = err.Error()
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.ErrorCode != 0 {
|
||||||
|
if e.ErrorCode == 4126 {
|
||||||
|
// refresh_token invalid, re-login
|
||||||
|
return d.login()
|
||||||
|
}
|
||||||
|
d.Status = e.Error
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return errors.New(e.Error)
|
||||||
|
}
|
||||||
|
data := res.Body()
|
||||||
|
d.Status = "work"
|
||||||
|
d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
|
||||||
|
d.AccessToken = jsoniter.Get(data, "access_token").ToString()
|
||||||
|
op.MustSaveDriverStorage(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||||
req := base.RestyClient.R()
|
req := base.RestyClient.R()
|
||||||
|
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
|
||||||
token, err := d.oauth2Token.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken)
|
|
||||||
|
|
||||||
if callback != nil {
|
if callback != nil {
|
||||||
callback(req)
|
callback(req)
|
||||||
}
|
}
|
||||||
@@ -30,6 +82,14 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if e.ErrorCode != 0 {
|
if e.ErrorCode != 0 {
|
||||||
|
if e.ErrorCode == 16 {
|
||||||
|
// login / refresh token
|
||||||
|
err = d.refreshToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d.request(url, method, callback, resp)
|
||||||
|
}
|
||||||
return nil, errors.New(e.Error)
|
return nil, errors.New(e.Error)
|
||||||
}
|
}
|
||||||
return res.Body(), nil
|
return res.Body(), nil
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ func (d *QuarkOrUC) Put(ctx context.Context, dstDir model.Obj, stream model.File
|
|||||||
_ = tempFile.Close()
|
_ = tempFile.Close()
|
||||||
}()
|
}()
|
||||||
m := md5.New()
|
m := md5.New()
|
||||||
_, err = utils.CopyWithBuffer(m, tempFile)
|
_, err = io.Copy(m, tempFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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))
|
md5Str := hex.EncodeToString(m.Sum(nil))
|
||||||
s := sha1.New()
|
s := sha1.New()
|
||||||
_, err = utils.CopyWithBuffer(s, tempFile)
|
_, err = io.Copy(s, tempFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,437 +0,0 @@
|
|||||||
package quqi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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/utils/random"
|
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
|
||||||
"github.com/aws/aws-sdk-go/aws/session"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3"
|
|
||||||
"github.com/aws/aws-sdk-go/service/s3/s3manager"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Quqi struct {
|
|
||||||
model.Storage
|
|
||||||
Addition
|
|
||||||
Cookie string // Cookie
|
|
||||||
GroupID string // 私人云群组ID
|
|
||||||
ClientID string // 随机生成客户端ID 经过测试,部分接口调用若不携带client id会出现错误
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Config() driver.Config {
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) GetAddition() driver.Additional {
|
|
||||||
return &d.Addition
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Init(ctx context.Context) error {
|
|
||||||
// 登录
|
|
||||||
if err := d.login(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成随机client id (与网页端生成逻辑一致)
|
|
||||||
d.ClientID = "quqipc_" + random.String(10)
|
|
||||||
|
|
||||||
// 获取私人云ID (暂时仅获取私人云)
|
|
||||||
groupResp := &GroupRes{}
|
|
||||||
if _, err := d.request("group.quqi.com", "/v1/group/list", resty.MethodGet, nil, groupResp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, groupInfo := range groupResp.Data {
|
|
||||||
if groupInfo == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if groupInfo.Type == 2 {
|
|
||||||
d.GroupID = strconv.Itoa(groupInfo.ID)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if d.GroupID == "" {
|
|
||||||
return errs.StorageNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Drop(ctx context.Context) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
|
||||||
var (
|
|
||||||
listResp = &ListRes{}
|
|
||||||
files []model.Obj
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := d.request("", "/api/dir/ls", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": dir.GetID(),
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, listResp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if listResp.Data == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// dirs
|
|
||||||
for _, dirInfo := range listResp.Data.Dir {
|
|
||||||
if dirInfo == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
files = append(files, &model.Object{
|
|
||||||
ID: strconv.FormatInt(dirInfo.NodeID, 10),
|
|
||||||
Name: dirInfo.Name,
|
|
||||||
Modified: time.Unix(dirInfo.UpdateTime, 0),
|
|
||||||
Ctime: time.Unix(dirInfo.AddTime, 0),
|
|
||||||
IsFolder: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// files
|
|
||||||
for _, fileInfo := range listResp.Data.File {
|
|
||||||
if fileInfo == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fileInfo.EXT != "" {
|
|
||||||
fileInfo.Name = strings.Join([]string{fileInfo.Name, fileInfo.EXT}, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
files = append(files, &model.Object{
|
|
||||||
ID: strconv.FormatInt(fileInfo.NodeID, 10),
|
|
||||||
Name: fileInfo.Name,
|
|
||||||
Size: fileInfo.Size,
|
|
||||||
Modified: time.Unix(fileInfo.UpdateTime, 0),
|
|
||||||
Ctime: time.Unix(fileInfo.AddTime, 0),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
|
||||||
if d.CDN {
|
|
||||||
link, err := d.linkFromCDN(file.GetID())
|
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
} else {
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
link, err := d.linkFromPreview(file.GetID())
|
|
||||||
if err != nil {
|
|
||||||
log.Warn(err)
|
|
||||||
} else {
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
link, err = d.linkFromDownload(file.GetID())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return link, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
|
|
||||||
var (
|
|
||||||
makeDirRes = &MakeDirRes{}
|
|
||||||
timeNow = time.Now()
|
|
||||||
)
|
|
||||||
|
|
||||||
if _, err := d.request("", "/api/dir/mkDir", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"parent_id": parentDir.GetID(),
|
|
||||||
"name": dirName,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, makeDirRes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Object{
|
|
||||||
ID: strconv.FormatInt(makeDirRes.Data.NodeID, 10),
|
|
||||||
Name: dirName,
|
|
||||||
Modified: timeNow,
|
|
||||||
Ctime: timeNow,
|
|
||||||
IsFolder: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
|
||||||
var moveRes = &MoveRes{}
|
|
||||||
|
|
||||||
if _, err := d.request("", "/api/dir/mvDir", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": dstDir.GetID(),
|
|
||||||
"source_quqi_id": d.GroupID,
|
|
||||||
"source_tree_id": "1",
|
|
||||||
"source_node_id": srcObj.GetID(),
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, moveRes); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Object{
|
|
||||||
ID: strconv.FormatInt(moveRes.Data.NodeID, 10),
|
|
||||||
Name: moveRes.Data.NodeName,
|
|
||||||
Size: srcObj.GetSize(),
|
|
||||||
Modified: time.Now(),
|
|
||||||
Ctime: srcObj.CreateTime(),
|
|
||||||
IsFolder: srcObj.IsDir(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
|
|
||||||
var realName = newName
|
|
||||||
|
|
||||||
if !srcObj.IsDir() {
|
|
||||||
srcExt, newExt := utils.Ext(srcObj.GetName()), utils.Ext(newName)
|
|
||||||
|
|
||||||
// 曲奇网盘的文件名称由文件名和扩展名组成,若存在扩展名,则重命名时仅支持更改文件名,扩展名在曲奇服务端保留
|
|
||||||
if srcExt != "" && srcExt == newExt {
|
|
||||||
parts := strings.Split(newName, ".")
|
|
||||||
if len(parts) > 1 {
|
|
||||||
realName = strings.Join(parts[:len(parts)-1], ".")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := d.request("", "/api/dir/renameDir", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": srcObj.GetID(),
|
|
||||||
"rename": realName,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Object{
|
|
||||||
ID: srcObj.GetID(),
|
|
||||||
Name: newName,
|
|
||||||
Size: srcObj.GetSize(),
|
|
||||||
Modified: time.Now(),
|
|
||||||
Ctime: srcObj.CreateTime(),
|
|
||||||
IsFolder: srcObj.IsDir(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Copy(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
|
|
||||||
// 无法从曲奇接口响应中直接获取复制后的文件信息
|
|
||||||
if _, err := d.request("", "/api/node/copy", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": dstDir.GetID(),
|
|
||||||
"source_quqi_id": d.GroupID,
|
|
||||||
"source_tree_id": "1",
|
|
||||||
"source_node_id": srcObj.GetID(),
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, nil); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Remove(ctx context.Context, obj model.Obj) error {
|
|
||||||
// 暂时不做直接删除,默认都放到回收站。直接删除方法:先调用删除接口放入回收站,在通过回收站接口删除文件
|
|
||||||
if _, err := d.request("", "/api/node/del", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": obj.GetID(),
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
|
|
||||||
// base info
|
|
||||||
sizeStr := strconv.FormatInt(stream.GetSize(), 10)
|
|
||||||
f, err := stream.CacheFullInTempFile()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
md5, err := utils.HashFile(utils.MD5, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sha, err := utils.HashFile(utils.SHA256, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// init upload
|
|
||||||
var uploadInitResp UploadInitResp
|
|
||||||
_, err = d.request("", "/api/upload/v1/file/init", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"parent_id": dstDir.GetID(),
|
|
||||||
"size": sizeStr,
|
|
||||||
"file_name": stream.GetName(),
|
|
||||||
"md5": md5,
|
|
||||||
"sha": sha,
|
|
||||||
"is_slice": "true",
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, &uploadInitResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// check exist
|
|
||||||
// if the file already exists in Quqi server, there is no need to actually upload it
|
|
||||||
if uploadInitResp.Data.Exist {
|
|
||||||
// the file name returned by Quqi does not include the extension name
|
|
||||||
nodeName, nodeExt := uploadInitResp.Data.NodeName, rawExt(stream.GetName())
|
|
||||||
if nodeExt != "" {
|
|
||||||
nodeName = nodeName + "." + nodeExt
|
|
||||||
}
|
|
||||||
return &model.Object{
|
|
||||||
ID: strconv.FormatInt(uploadInitResp.Data.NodeID, 10),
|
|
||||||
Name: nodeName,
|
|
||||||
Size: stream.GetSize(),
|
|
||||||
Modified: stream.ModTime(),
|
|
||||||
Ctime: stream.CreateTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
// listParts
|
|
||||||
_, err = d.request("upload.quqi.com:20807", "/upload/v1/listParts", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"token": uploadInitResp.Data.Token,
|
|
||||||
"task_id": uploadInitResp.Data.TaskID,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// get temp key
|
|
||||||
var tempKeyResp TempKeyResp
|
|
||||||
_, err = d.request("upload.quqi.com:20807", "/upload/v1/tempKey", resty.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"token": uploadInitResp.Data.Token,
|
|
||||||
"task_id": uploadInitResp.Data.TaskID,
|
|
||||||
})
|
|
||||||
}, &tempKeyResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// upload
|
|
||||||
// u, err := url.Parse(fmt.Sprintf("https://%s.cos.ap-shanghai.myqcloud.com", uploadInitResp.Data.Bucket))
|
|
||||||
// b := &cos.BaseURL{BucketURL: u}
|
|
||||||
// client := cos.NewClient(b, &http.Client{
|
|
||||||
// Transport: &cos.CredentialTransport{
|
|
||||||
// Credential: cos.NewTokenCredential(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken),
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
// partSize := int64(1024 * 1024 * 2)
|
|
||||||
// partCount := (stream.GetSize() + partSize - 1) / partSize
|
|
||||||
// for i := 1; i <= int(partCount); i++ {
|
|
||||||
// length := partSize
|
|
||||||
// if i == int(partCount) {
|
|
||||||
// length = stream.GetSize() - (int64(i)-1)*partSize
|
|
||||||
// }
|
|
||||||
// _, err := client.Object.UploadPart(
|
|
||||||
// ctx, uploadInitResp.Data.Key, uploadInitResp.Data.UploadID, i, io.LimitReader(f, partSize), &cos.ObjectUploadPartOptions{
|
|
||||||
// ContentLength: length,
|
|
||||||
// },
|
|
||||||
// )
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
cfg := &aws.Config{
|
|
||||||
Credentials: credentials.NewStaticCredentials(tempKeyResp.Data.Credentials.TmpSecretID, tempKeyResp.Data.Credentials.TmpSecretKey, tempKeyResp.Data.Credentials.SessionToken),
|
|
||||||
Region: aws.String("ap-shanghai"),
|
|
||||||
Endpoint: aws.String("cos.ap-shanghai.myqcloud.com"),
|
|
||||||
}
|
|
||||||
s, err := session.NewSession(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
uploader := s3manager.NewUploader(s)
|
|
||||||
buf := make([]byte, 1024*1024*2)
|
|
||||||
for partNumber := int64(1); ; partNumber++ {
|
|
||||||
n, err := io.ReadFull(f, buf)
|
|
||||||
if err != nil && err != io.ErrUnexpectedEOF {
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = uploader.S3.UploadPartWithContext(ctx, &s3.UploadPartInput{
|
|
||||||
UploadId: &uploadInitResp.Data.UploadID,
|
|
||||||
Key: &uploadInitResp.Data.Key,
|
|
||||||
Bucket: &uploadInitResp.Data.Bucket,
|
|
||||||
PartNumber: aws.Int64(partNumber),
|
|
||||||
Body: bytes.NewReader(buf[:n]),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// finish upload
|
|
||||||
var uploadFinishResp UploadFinishResp
|
|
||||||
_, err = d.request("", "/api/upload/v1/file/finish", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"token": uploadInitResp.Data.Token,
|
|
||||||
"task_id": uploadInitResp.Data.TaskID,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, &uploadFinishResp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// the file name returned by Quqi does not include the extension name
|
|
||||||
nodeName, nodeExt := uploadFinishResp.Data.NodeName, rawExt(stream.GetName())
|
|
||||||
if nodeExt != "" {
|
|
||||||
nodeName = nodeName + "." + nodeExt
|
|
||||||
}
|
|
||||||
return &model.Object{
|
|
||||||
ID: strconv.FormatInt(uploadFinishResp.Data.NodeID, 10),
|
|
||||||
Name: nodeName,
|
|
||||||
Size: stream.GetSize(),
|
|
||||||
Modified: stream.ModTime(),
|
|
||||||
Ctime: stream.CreateTime(),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
|
|
||||||
// return nil, errs.NotSupport
|
|
||||||
//}
|
|
||||||
|
|
||||||
var _ driver.Driver = (*Quqi)(nil)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package quqi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/alist-org/alist/v3/internal/driver"
|
|
||||||
"github.com/alist-org/alist/v3/internal/op"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Addition struct {
|
|
||||||
driver.RootID
|
|
||||||
Phone string `json:"phone"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Cookie string `json:"cookie" help:"Cookie can be used on multiple clients at the same time"`
|
|
||||||
CDN bool `json:"cdn" help:"If you enable this option, the download speed can be increased, but there will be some performance loss"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var config = driver.Config{
|
|
||||||
Name: "Quqi",
|
|
||||||
OnlyLocal: true,
|
|
||||||
LocalSort: true,
|
|
||||||
//NoUpload: true,
|
|
||||||
DefaultRoot: "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
op.RegisterDriver(func() driver.Driver {
|
|
||||||
return &Quqi{}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
package quqi
|
|
||||||
|
|
||||||
type BaseReqQuery struct {
|
|
||||||
ID string `json:"quqiid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseReq struct {
|
|
||||||
GroupID string `json:"quqi_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseRes struct {
|
|
||||||
//Data interface{} `json:"data"`
|
|
||||||
Code int `json:"err"`
|
|
||||||
Message string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GroupRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data []*Group `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data *List `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDocRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
OriginPath string `json:"origin_path"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetDownloadResp struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
Url string `json:"url"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MakeDirRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
IsRoot bool `json:"is_root"`
|
|
||||||
NodeID int64 `json:"node_id"`
|
|
||||||
ParentID int64 `json:"parent_id"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MoveRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
NodeChildNum int64 `json:"node_child_num"`
|
|
||||||
NodeID int64 `json:"node_id"`
|
|
||||||
NodeName string `json:"node_name"`
|
|
||||||
ParentID int64 `json:"parent_id"`
|
|
||||||
GroupID int64 `json:"quqi_id"`
|
|
||||||
TreeID int64 `json:"tree_id"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RenameRes struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
NodeID int64 `json:"node_id"`
|
|
||||||
GroupID int64 `json:"quqi_id"`
|
|
||||||
Rename string `json:"rename"`
|
|
||||||
TreeID int64 `json:"tree_id"`
|
|
||||||
UpdateTime int64 `json:"updatetime"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CopyRes struct {
|
|
||||||
BaseRes
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoveRes struct {
|
|
||||||
BaseRes
|
|
||||||
}
|
|
||||||
|
|
||||||
type Group struct {
|
|
||||||
ID int `json:"quqi_id"`
|
|
||||||
Type int `json:"type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
IsAdministrator int `json:"is_administrator"`
|
|
||||||
Role int `json:"role"`
|
|
||||||
Avatar string `json:"avatar_url"`
|
|
||||||
IsStick int `json:"is_stick"`
|
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
Status int `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type List struct {
|
|
||||||
ListDir
|
|
||||||
Dir []*ListDir `json:"dir"`
|
|
||||||
File []*ListFile `json:"file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListItem struct {
|
|
||||||
AddTime int64 `json:"add_time"`
|
|
||||||
IsDir int `json:"is_dir"`
|
|
||||||
IsExpand int `json:"is_expand"`
|
|
||||||
IsFinalize int `json:"is_finalize"`
|
|
||||||
LastEditorName string `json:"last_editor_name"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
NodeID int64 `json:"nid"`
|
|
||||||
ParentID int64 `json:"parent_id"`
|
|
||||||
Permission int `json:"permission"`
|
|
||||||
TreeID int64 `json:"tid"`
|
|
||||||
UpdateCNT int64 `json:"update_cnt"`
|
|
||||||
UpdateTime int64 `json:"update_time"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListDir struct {
|
|
||||||
ListItem
|
|
||||||
ChildDocNum int64 `json:"child_doc_num"`
|
|
||||||
DirDetail string `json:"dir_detail"`
|
|
||||||
DirType int `json:"dir_type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ListFile struct {
|
|
||||||
ListItem
|
|
||||||
BroadDocType string `json:"broad_doc_type"`
|
|
||||||
CanDisplay bool `json:"can_display"`
|
|
||||||
Detail string `json:"detail"`
|
|
||||||
EXT string `json:"ext"`
|
|
||||||
Filetype string `json:"filetype"`
|
|
||||||
HasMobileThumbnail bool `json:"has_mobile_thumbnail"`
|
|
||||||
HasThumbnail bool `json:"has_thumbnail"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
Version int `json:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UploadInitResp struct {
|
|
||||||
Data struct {
|
|
||||||
Bucket string `json:"bucket"`
|
|
||||||
Exist bool `json:"exist"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
TaskID string `json:"task_id"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
UploadID string `json:"upload_id"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
NodeID int64 `json:"node_id"`
|
|
||||||
NodeName string `json:"node_name"`
|
|
||||||
ParentID int64 `json:"parent_id"`
|
|
||||||
} `json:"data"`
|
|
||||||
Err int `json:"err"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TempKeyResp struct {
|
|
||||||
Err int `json:"err"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data struct {
|
|
||||||
ExpiredTime int `json:"expiredTime"`
|
|
||||||
Expiration string `json:"expiration"`
|
|
||||||
Credentials struct {
|
|
||||||
SessionToken string `json:"sessionToken"`
|
|
||||||
TmpSecretID string `json:"tmpSecretId"`
|
|
||||||
TmpSecretKey string `json:"tmpSecretKey"`
|
|
||||||
} `json:"credentials"`
|
|
||||||
RequestID string `json:"requestId"`
|
|
||||||
StartTime int `json:"startTime"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UploadFinishResp struct {
|
|
||||||
Data struct {
|
|
||||||
NodeID int64 `json:"node_id"`
|
|
||||||
NodeName string `json:"node_name"`
|
|
||||||
ParentID int64 `json:"parent_id"`
|
|
||||||
QuqiID int64 `json:"quqi_id"`
|
|
||||||
TreeID int64 `json:"tree_id"`
|
|
||||||
} `json:"data"`
|
|
||||||
Err int `json:"err"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UrlExchangeResp struct {
|
|
||||||
BaseRes
|
|
||||||
Data struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Mime string `json:"mime"`
|
|
||||||
Size int64 `json:"size"`
|
|
||||||
DownloadType int `json:"download_type"`
|
|
||||||
ChannelType int `json:"channel_type"`
|
|
||||||
ChannelID int `json:"channel_id"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
ExpiredTime int64 `json:"expired_time"`
|
|
||||||
IsEncrypted bool `json:"is_encrypted"`
|
|
||||||
EncryptedSize int64 `json:"encrypted_size"`
|
|
||||||
EncryptedAlg string `json:"encrypted_alg"`
|
|
||||||
EncryptedKey string `json:"encrypted_key"`
|
|
||||||
PassportID int64 `json:"passport_id"`
|
|
||||||
RequestExpiredTime int64 `json:"request_expired_time"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
package quqi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
stdpath "path"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
|
||||||
"github.com/alist-org/alist/v3/internal/stream"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/http_range"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"github.com/go-resty/resty/v2"
|
|
||||||
"github.com/minio/sio"
|
|
||||||
)
|
|
||||||
|
|
||||||
// do others that not defined in Driver interface
|
|
||||||
func (d *Quqi) request(host string, path string, method string, callback base.ReqCallback, resp interface{}) (*resty.Response, error) {
|
|
||||||
var (
|
|
||||||
reqUrl = url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "quqi.com",
|
|
||||||
Path: path,
|
|
||||||
}
|
|
||||||
req = base.RestyClient.R()
|
|
||||||
result BaseRes
|
|
||||||
)
|
|
||||||
|
|
||||||
if host != "" {
|
|
||||||
reqUrl.Host = host
|
|
||||||
}
|
|
||||||
req.SetHeaders(map[string]string{
|
|
||||||
"Origin": "https://quqi.com",
|
|
||||||
"Cookie": d.Cookie,
|
|
||||||
})
|
|
||||||
|
|
||||||
if d.GroupID != "" {
|
|
||||||
req.SetQueryParam("quqiid", d.GroupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if callback != nil {
|
|
||||||
callback(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := req.Execute(method, reqUrl.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// resty.Request.SetResult cannot parse result correctly sometimes
|
|
||||||
err = utils.Json.Unmarshal(res.Body(), &result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if result.Code != 0 {
|
|
||||||
return nil, errors.New(result.Message)
|
|
||||||
}
|
|
||||||
if resp != nil {
|
|
||||||
err = utils.Json.Unmarshal(res.Body(), resp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) login() error {
|
|
||||||
if d.Addition.Cookie != "" {
|
|
||||||
d.Cookie = d.Addition.Cookie
|
|
||||||
}
|
|
||||||
if d.checkLogin() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if d.Cookie != "" {
|
|
||||||
return errors.New("cookie is invalid")
|
|
||||||
}
|
|
||||||
if d.Phone == "" {
|
|
||||||
return errors.New("phone number is empty")
|
|
||||||
}
|
|
||||||
if d.Password == "" {
|
|
||||||
return errs.EmptyPassword
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := d.request("", "/auth/person/v2/login/password", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"phone": d.Phone,
|
|
||||||
"password": base64.StdEncoding.EncodeToString([]byte(d.Password)),
|
|
||||||
})
|
|
||||||
}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookies []string
|
|
||||||
for _, cookie := range resp.RawResponse.Cookies() {
|
|
||||||
cookies = append(cookies, fmt.Sprintf("%s=%s", cookie.Name, cookie.Value))
|
|
||||||
}
|
|
||||||
d.Cookie = strings.Join(cookies, ";")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) checkLogin() bool {
|
|
||||||
if _, err := d.request("", "/auth/account/baseInfo", resty.MethodGet, nil, nil); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// rawExt 保留扩展名大小写
|
|
||||||
func rawExt(name string) string {
|
|
||||||
ext := stdpath.Ext(name)
|
|
||||||
if strings.HasPrefix(ext, ".") {
|
|
||||||
ext = ext[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ext
|
|
||||||
}
|
|
||||||
|
|
||||||
// decryptKey 获取密码
|
|
||||||
func decryptKey(encodeKey string) []byte {
|
|
||||||
// 移除非法字符
|
|
||||||
u := strings.ReplaceAll(encodeKey, "[^A-Za-z0-9+\\/]", "")
|
|
||||||
|
|
||||||
// 计算输出字节数组的长度
|
|
||||||
o := len(u)
|
|
||||||
a := 32
|
|
||||||
|
|
||||||
// 创建输出字节数组
|
|
||||||
c := make([]byte, a)
|
|
||||||
|
|
||||||
// 编码循环
|
|
||||||
s := uint32(0) // 累加器
|
|
||||||
f := 0 // 输出数组索引
|
|
||||||
for l := 0; l < o; l++ {
|
|
||||||
r := l & 3 // 取模4,得到当前字符在四字节块中的位置
|
|
||||||
i := u[l] // 当前字符的ASCII码
|
|
||||||
|
|
||||||
// 编码当前字符
|
|
||||||
switch {
|
|
||||||
case i >= 65 && i < 91: // 大写字母
|
|
||||||
s |= uint32(i-65) << uint32(6*(3-r))
|
|
||||||
case i >= 97 && i < 123: // 小写字母
|
|
||||||
s |= uint32(i-71) << uint32(6*(3-r))
|
|
||||||
case i >= 48 && i < 58: // 数字
|
|
||||||
s |= uint32(i+4) << uint32(6*(3-r))
|
|
||||||
case i == 43: // 加号
|
|
||||||
s |= uint32(62) << uint32(6*(3-r))
|
|
||||||
case i == 47: // 斜杠
|
|
||||||
s |= uint32(63) << uint32(6*(3-r))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果累加器已经包含了四个字符,或者是最后一个字符,则写入输出数组
|
|
||||||
if r == 3 || l == o-1 {
|
|
||||||
for e := 0; e < 3 && f < a; e, f = e+1, f+1 {
|
|
||||||
c[f] = byte(s >> (16 >> e & 24) & 255)
|
|
||||||
}
|
|
||||||
s = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) linkFromPreview(id string) (*model.Link, error) {
|
|
||||||
var getDocResp GetDocRes
|
|
||||||
if _, err := d.request("", "/api/doc/getDoc", resty.MethodPost, func(req *resty.Request) {
|
|
||||||
req.SetFormData(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": id,
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
})
|
|
||||||
}, &getDocResp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if getDocResp.Data.OriginPath == "" {
|
|
||||||
return nil, errors.New("cannot get link from preview")
|
|
||||||
}
|
|
||||||
return &model.Link{
|
|
||||||
URL: getDocResp.Data.OriginPath,
|
|
||||||
Header: http.Header{
|
|
||||||
"Origin": []string{"https://quqi.com"},
|
|
||||||
"Cookie": []string{d.Cookie},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) linkFromDownload(id string) (*model.Link, error) {
|
|
||||||
var getDownloadResp GetDownloadResp
|
|
||||||
if _, err := d.request("", "/api/doc/getDownload", resty.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParams(map[string]string{
|
|
||||||
"quqi_id": d.GroupID,
|
|
||||||
"tree_id": "1",
|
|
||||||
"node_id": id,
|
|
||||||
"url_type": "undefined",
|
|
||||||
"entry_type": "undefined",
|
|
||||||
"client_id": d.ClientID,
|
|
||||||
"no_redirect": "1",
|
|
||||||
})
|
|
||||||
}, &getDownloadResp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if getDownloadResp.Data.Url == "" {
|
|
||||||
return nil, errors.New("cannot get link from download")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Link{
|
|
||||||
URL: getDownloadResp.Data.Url,
|
|
||||||
Header: http.Header{
|
|
||||||
"Origin": []string{"https://quqi.com"},
|
|
||||||
"Cookie": []string{d.Cookie},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Quqi) linkFromCDN(id string) (*model.Link, error) {
|
|
||||||
downloadLink, err := d.linkFromDownload(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var urlExchangeResp UrlExchangeResp
|
|
||||||
if _, err = d.request("api.quqi.com", "/preview/downloadInfo/url/exchange", resty.MethodGet, func(req *resty.Request) {
|
|
||||||
req.SetQueryParam("url", downloadLink.URL)
|
|
||||||
}, &urlExchangeResp); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if urlExchangeResp.Data.Url == "" {
|
|
||||||
return nil, errors.New("cannot get link from cdn")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 假设存在未加密的情况
|
|
||||||
if !urlExchangeResp.Data.IsEncrypted {
|
|
||||||
return &model.Link{
|
|
||||||
URL: urlExchangeResp.Data.Url,
|
|
||||||
Header: http.Header{
|
|
||||||
"Origin": []string{"https://quqi.com"},
|
|
||||||
"Cookie": []string{d.Cookie},
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据sio(https://github.com/minio/sio/blob/master/DARE.md)描述及实际测试,得出以下结论:
|
|
||||||
// 1. 加密后大小(encrypted_size)-原始文件大小(size) = 加密包的头大小+身份验证标识 = (16+16) * N -> N为加密包的数量
|
|
||||||
// 2. 原始文件大小(size)+64*1024-1 / (64*1024) = N -> 每个包的有效负载为64K
|
|
||||||
remoteClosers := utils.EmptyClosers()
|
|
||||||
payloadSize := int64(1 << 16)
|
|
||||||
expiration := time.Until(time.Unix(urlExchangeResp.Data.ExpiredTime, 0))
|
|
||||||
resultRangeReader := func(ctx context.Context, httpRange http_range.Range) (io.ReadCloser, error) {
|
|
||||||
encryptedOffset := httpRange.Start / payloadSize * (payloadSize + 32)
|
|
||||||
decryptedOffset := httpRange.Start % payloadSize
|
|
||||||
encryptedLength := (httpRange.Length+httpRange.Start+payloadSize-1)/payloadSize*(payloadSize+32) - encryptedOffset
|
|
||||||
if httpRange.Length < 0 {
|
|
||||||
encryptedLength = httpRange.Length
|
|
||||||
} else {
|
|
||||||
if httpRange.Length+httpRange.Start >= urlExchangeResp.Data.Size || encryptedLength+encryptedOffset >= urlExchangeResp.Data.EncryptedSize {
|
|
||||||
encryptedLength = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//log.Debugf("size: %d\tencrypted_size: %d", urlExchangeResp.Data.Size, urlExchangeResp.Data.EncryptedSize)
|
|
||||||
//log.Debugf("http range offset: %d, length: %d", httpRange.Start, httpRange.Length)
|
|
||||||
//log.Debugf("encrypted offset: %d, length: %d, decrypted offset: %d", encryptedOffset, encryptedLength, decryptedOffset)
|
|
||||||
|
|
||||||
rrc, err := stream.GetRangeReadCloserFromLink(urlExchangeResp.Data.EncryptedSize, &model.Link{
|
|
||||||
URL: urlExchangeResp.Data.Url,
|
|
||||||
Header: http.Header{
|
|
||||||
"Origin": []string{"https://quqi.com"},
|
|
||||||
"Cookie": []string{d.Cookie},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rc, err := rrc.RangeRead(ctx, http_range.Range{Start: encryptedOffset, Length: encryptedLength})
|
|
||||||
remoteClosers.AddClosers(rrc.GetClosers())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptReader, err := sio.DecryptReader(rc, sio.Config{
|
|
||||||
MinVersion: sio.Version10,
|
|
||||||
MaxVersion: sio.Version20,
|
|
||||||
CipherSuites: []byte{sio.CHACHA20_POLY1305, sio.AES_256_GCM},
|
|
||||||
Key: decryptKey(urlExchangeResp.Data.EncryptedKey),
|
|
||||||
SequenceNumber: uint32(httpRange.Start / payloadSize),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
bufferReader := bufio.NewReader(decryptReader)
|
|
||||||
bufferReader.Discard(int(decryptedOffset))
|
|
||||||
|
|
||||||
return utils.NewReadCloser(bufferReader, func() error {
|
|
||||||
return nil
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &model.Link{
|
|
||||||
Header: http.Header{
|
|
||||||
"Origin": []string{"https://quqi.com"},
|
|
||||||
"Cookie": []string{d.Cookie},
|
|
||||||
},
|
|
||||||
RangeReadCloser: &model.RangeReadCloser{RangeReader: resultRangeReader, Closers: remoteClosers},
|
|
||||||
Expiration: &expiration,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/internal/stream"
|
"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/driver"
|
||||||
"github.com/alist-org/alist/v3/internal/model"
|
"github.com/alist-org/alist/v3/internal/model"
|
||||||
@@ -27,13 +26,10 @@ type S3 struct {
|
|||||||
Session *session.Session
|
Session *session.Session
|
||||||
client *s3.S3
|
client *s3.S3
|
||||||
linkClient *s3.S3
|
linkClient *s3.S3
|
||||||
|
|
||||||
config driver.Config
|
|
||||||
cron *cron.Cron
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *S3) Config() driver.Config {
|
func (d *S3) Config() driver.Config {
|
||||||
return d.config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *S3) GetAddition() driver.Additional {
|
func (d *S3) GetAddition() driver.Additional {
|
||||||
@@ -44,18 +40,6 @@ func (d *S3) Init(ctx context.Context) error {
|
|||||||
if d.Region == "" {
|
if d.Region == "" {
|
||||||
d.Region = "alist"
|
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()
|
err := d.initSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -66,9 +50,6 @@ func (d *S3) Init(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *S3) Drop(ctx context.Context) error {
|
func (d *S3) Drop(ctx context.Context) error {
|
||||||
if d.cron != nil {
|
|
||||||
d.cron.Stop()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,25 +22,15 @@ type Addition struct {
|
|||||||
AddFilenameToDisposition bool `json:"add_filename_to_disposition" help:"Add filename to Content-Disposition header."`
|
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() {
|
func init() {
|
||||||
op.RegisterDriver(func() driver.Driver {
|
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,21 +21,13 @@ import (
|
|||||||
// do others that not defined in Driver interface
|
// do others that not defined in Driver interface
|
||||||
|
|
||||||
func (d *S3) initSession() error {
|
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{
|
cfg := &aws.Config{
|
||||||
Credentials: credentials.NewStaticCredentials(accessKeyID, secretAccessKey, sessionToken),
|
Credentials: credentials.NewStaticCredentials(d.AccessKeyID, d.SecretAccessKey, d.SessionToken),
|
||||||
Region: &d.Region,
|
Region: &d.Region,
|
||||||
Endpoint: &d.Endpoint,
|
Endpoint: &d.Endpoint,
|
||||||
S3ForcePathStyle: aws.Bool(d.ForcePathStyle),
|
S3ForcePathStyle: aws.Bool(d.ForcePathStyle),
|
||||||
}
|
}
|
||||||
|
var err error
|
||||||
d.Session, err = session.NewSession(cfg)
|
d.Session, err = session.NewSession(cfg)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,7 +19,6 @@ type Seafile struct {
|
|||||||
Addition
|
Addition
|
||||||
|
|
||||||
authorization string
|
authorization string
|
||||||
libraryMap map[string]*LibraryInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Config() driver.Config {
|
func (d *Seafile) Config() driver.Config {
|
||||||
@@ -31,8 +31,6 @@ func (d *Seafile) GetAddition() driver.Additional {
|
|||||||
|
|
||||||
func (d *Seafile) Init(ctx context.Context) error {
|
func (d *Seafile) Init(ctx context.Context) error {
|
||||||
d.Address = strings.TrimSuffix(d.Address, "/")
|
d.Address = strings.TrimSuffix(d.Address, "/")
|
||||||
d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath)
|
|
||||||
d.libraryMap = make(map[string]*LibraryInfo)
|
|
||||||
return d.getToken()
|
return d.getToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,37 +38,10 @@ func (d *Seafile) Drop(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) (result []model.Obj, err error) {
|
func (d *Seafile) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
path := dir.GetPath()
|
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
|
var resp []RepoDirItemResp
|
||||||
_, err = d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", repo.Id), func(req *resty.Request) {
|
_, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
req.SetResult(&resp).SetQueryParams(map[string]string{
|
req.SetResult(&resp).SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": path,
|
||||||
})
|
})
|
||||||
@@ -92,13 +63,9 @@ 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) {
|
func (d *Seafile) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
repo, path, err := d.getRepoAndPath(file.GetPath())
|
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": file.GetPath(),
|
||||||
"reuse": "1",
|
"reuse": "1",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -111,14 +78,9 @@ 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 {
|
func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
repo, path, err := d.getRepoAndPath(parentDir.GetPath())
|
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/dir/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": filepath.Join(parentDir.GetPath(), dirName),
|
||||||
}).SetFormData(map[string]string{
|
}).SetFormData(map[string]string{
|
||||||
"operation": "mkdir",
|
"operation": "mkdir",
|
||||||
})
|
})
|
||||||
@@ -127,34 +89,22 @@ func (d *Seafile) MakeDir(ctx context.Context, parentDir model.Obj, dirName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *Seafile) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": srcObj.GetPath(),
|
||||||
}).SetFormData(map[string]string{
|
}).SetFormData(map[string]string{
|
||||||
"operation": "move",
|
"operation": "move",
|
||||||
"dst_repo": dstRepo.Id,
|
"dst_repo": d.Addition.RepoId,
|
||||||
"dst_dir": dstPath,
|
"dst_dir": dstDir.GetPath(),
|
||||||
})
|
})
|
||||||
}, true)
|
}, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": srcObj.GetPath(),
|
||||||
}).SetFormData(map[string]string{
|
}).SetFormData(map[string]string{
|
||||||
"operation": "rename",
|
"operation": "rename",
|
||||||
"newname": newName,
|
"newname": newName,
|
||||||
@@ -164,47 +114,31 @@ func (d *Seafile) Rename(ctx context.Context, srcObj model.Obj, newName string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *Seafile) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
repo, path, err := d.getRepoAndPath(srcObj.GetPath())
|
_, err := d.request(http.MethodPost, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": srcObj.GetPath(),
|
||||||
}).SetFormData(map[string]string{
|
}).SetFormData(map[string]string{
|
||||||
"operation": "copy",
|
"operation": "copy",
|
||||||
"dst_repo": dstRepo.Id,
|
"dst_repo": d.Addition.RepoId,
|
||||||
"dst_dir": dstPath,
|
"dst_dir": dstDir.GetPath(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *Seafile) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
repo, path, err := d.getRepoAndPath(obj.GetPath())
|
_, err := d.request(http.MethodDelete, fmt.Sprintf("/api2/repos/%s/file/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": obj.GetPath(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
repo, path, err := d.getRepoAndPath(dstDir.GetPath())
|
res, err := d.request(http.MethodGet, fmt.Sprintf("/api2/repos/%s/upload-link/", d.Addition.RepoId), func(req *resty.Request) {
|
||||||
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{
|
req.SetQueryParams(map[string]string{
|
||||||
"p": path,
|
"p": dstDir.GetPath(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -216,7 +150,7 @@ func (d *Seafile) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
|
|||||||
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
_, err = d.request(http.MethodPost, u, func(req *resty.Request) {
|
||||||
req.SetFileReader("file", stream.GetName(), stream).
|
req.SetFileReader("file", stream.GetName(), stream).
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"parent_dir": path,
|
"parent_dir": dstDir.GetPath(),
|
||||||
"replace": "1",
|
"replace": "1",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ type Addition struct {
|
|||||||
driver.RootPath
|
driver.RootPath
|
||||||
|
|
||||||
Address string `json:"address" required:"true"`
|
Address string `json:"address" required:"true"`
|
||||||
UserName string `json:"username" required:"false"`
|
UserName string `json:"username" required:"true"`
|
||||||
Password string `json:"password" required:"false"`
|
Password string `json:"password" required:"true"`
|
||||||
Token string `json:"token" required:"false"`
|
RepoId string `json:"repoId" required:"true"`
|
||||||
RepoId string `json:"repoId" required:"false"`
|
|
||||||
RepoPwd string `json:"repoPwd" required:"false"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
@@ -1,44 +1,14 @@
|
|||||||
package seafile
|
package seafile
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type AuthTokenResp struct {
|
type AuthTokenResp struct {
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RepoItemResp struct {
|
type RepoDirItemResp struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
Type string `json:"type"` // repo, dir, file
|
Type string `json:"type"` // dir, file
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Modified int64 `json:"mtime"`
|
Modified int64 `json:"mtime"`
|
||||||
Permission string `json:"permission"`
|
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
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,14 @@
|
|||||||
package seafile
|
package seafile
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/alist-org/alist/v3/internal/errs"
|
|
||||||
"github.com/alist-org/alist/v3/pkg/utils"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/alist-org/alist/v3/drivers/base"
|
"github.com/alist-org/alist/v3/drivers/base"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Seafile) getToken() error {
|
func (d *Seafile) getToken() error {
|
||||||
if d.Token != "" {
|
|
||||||
d.authorization = fmt.Sprintf("Token %s", d.Token)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var authResp AuthTokenResp
|
var authResp AuthTokenResp
|
||||||
res, err := base.RestyClient.R().
|
res, err := base.RestyClient.R().
|
||||||
SetResult(&authResp).
|
SetResult(&authResp).
|
||||||
@@ -69,110 +60,3 @@ func (d *Seafile) request(method string, pathname string, callback base.ReqCallb
|
|||||||
}
|
}
|
||||||
return res.Body(), nil
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import (
|
|||||||
type SFTP struct {
|
type SFTP struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
client *sftp.Client
|
client *sftp.Client
|
||||||
clientConnectionError error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Config() driver.Config {
|
func (d *SFTP) Config() driver.Config {
|
||||||
@@ -40,9 +39,6 @@ func (d *SFTP) Drop(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Debugf("[sftp] list dir: %s", dir.GetPath())
|
log.Debugf("[sftp] list dir: %s", dir.GetPath())
|
||||||
files, err := d.client.ReadDir(dir.GetPath())
|
files, err := d.client.ReadDir(dir.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,9 +51,6 @@ func (d *SFTP) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
remoteFile, err := d.client.Open(file.GetPath())
|
remoteFile, err := d.client.Open(file.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -69,23 +62,14 @@ func (d *SFTP) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
func (d *SFTP) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))
|
return d.client.MkdirAll(path.Join(parentDir.GetPath(), dirName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
func (d *SFTP) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))
|
return d.client.Rename(srcObj.GetPath(), path.Join(dstDir.GetPath(), srcObj.GetName()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
func (d *SFTP) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))
|
return d.client.Rename(srcObj.GetPath(), path.Join(path.Dir(srcObj.GetPath()), newName))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,16 +78,10 @@ func (d *SFTP) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {
|
func (d *SFTP) Remove(ctx context.Context, obj model.Obj) error {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return d.remove(obj.GetPath())
|
return d.remove(obj.GetPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
|
||||||
if err := d.clientReconnectOnConnectionError(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))
|
dstFile, err := d.client.Create(path.Join(dstDir.GetPath(), stream.GetName()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ type Addition struct {
|
|||||||
PrivateKey string `json:"private_key" type:"text"`
|
PrivateKey string `json:"private_key" type:"text"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
driver.RootPath
|
driver.RootPath
|
||||||
IgnoreSymlinkError bool `json:"ignore_symlink_error" default:"false" info:"Ignore symlink error"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var config = driver.Config{
|
var config = driver.Config{
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user