Compare commits

..

88 Commits

Author SHA1 Message Date
549355bb29 build: change golang version 2022-10-12 17:35:44 +08:00
55aa8ee3b1 fix: version print of build script [skip ci] 2022-10-12 17:24:04 +08:00
1c22fc367e docs: change badges in readme 2022-10-12 17:08:40 +08:00
5ea8d62aa4 fix(onedrive): unable to operate if path contains % (close #1965) 2022-10-11 14:21:58 +08:00
baebc2fbe9 fix: can't delete disabled storage (close #1942) 2022-10-09 22:20:48 +08:00
8c69260972 fix(webdav): set mime by ext if it's empty 2022-10-09 19:29:55 +08:00
30f992c6a8 feat(onedrive): customize chunk size (close #1927) 2022-10-08 22:23:33 +08:00
dcaaae366b feat: add support for mega.nz (close 1553) 2022-10-08 22:16:41 +08:00
284035823f feat: add Google Photo support (#1853)
* feat: add Google Photo support

* fix: fetch all pages

* chore(google_photo): add meta info

Co-authored-by: Noah Hsu <i@nn.ci>
2022-10-07 20:36:56 +08:00
be8ff92414 docs: replace qq with discord [skip ci] 2022-10-05 14:17:00 +08:00
a4c846a424 chore(onedrive): set default value for region 2022-10-01 20:09:57 +08:00
451e418b18 perf: return cache before check obj to reduce recursion 2022-09-28 21:19:36 +08:00
4e13b1a83c perf: modify onedrive upload chunk size (#1831 close #1790)
improve onedrive upload speed
2022-09-27 20:29:54 +08:00
9d2e9887af docs: create FUNDING.yml [skip ci] 2022-09-27 14:41:43 +08:00
dc73c2e97d fix: custom token expires in doesn't work 2022-09-27 14:23:56 +08:00
a624121095 ci: manual trigger github actions 2022-09-27 14:12:36 +08:00
9d9c79179b feat: custom token expires in 2022-09-27 14:05:00 +08:00
b7479651e1 fix: incorrect base_path from site_url (close #1830) 2022-09-27 13:56:32 +08:00
2fc0ccbfe0 fix: don't init aria2 in new goroutine (close #1752) 2022-09-26 15:11:08 +08:00
f86ad1dce4 fix: create temp dir perm with 777 (close #1813) 2022-09-26 14:48:59 +08:00
f0181d92cd fix: keep type of setting item is correct 2022-09-25 21:20:32 +08:00
5ac6a30c56 fix: use get_share_link_download_url if can't get_download_url (close #1753) 2022-09-25 20:32:11 +08:00
96d8a382e8 fix(aliyundrive_share): reget share token if token expired (close #1798) 2022-09-25 20:14:33 +08:00
7c32af4649 refactor!: move api_url and base_path to config file 2022-09-25 17:57:54 +08:00
03dbb3a403 chore: fix typo of env name 2022-09-25 17:41:04 +08:00
a570e4c7a0 fix: some settings don't take effect at startup 2022-09-23 20:37:49 +08:00
539c47bd3b chore: change log if aria2 not ready 2022-09-23 20:04:47 +08:00
b6d9018ebd fix: sorting by modified doesn't work (close #1756) 2022-09-23 12:30:32 +08:00
c929888e39 fix(123): change remove api (close #1760) 2022-09-23 12:28:57 +08:00
af946ff13e fix(baidu_photo): cannot download when proxy is opened 2022-09-23 01:15:12 +08:00
0039dc18e1 fix: set cdn to basePath if cdn is empty 2022-09-22 17:11:45 +08:00
4d6ab53336 feat: add form upload api (close #1693 #1709) 2022-09-22 16:53:58 +08:00
c7f6684eed chore: add provider to fs list resp 2022-09-22 16:04:10 +08:00
b71ecc8e89 chore: add a default polyfill to head 2022-09-22 11:29:39 +08:00
3537153b91 feat: add aliyundrive share driver (close #1215) 2022-09-21 22:00:06 +08:00
9382f66f87 fix(aliyundrive): thumbnail missed 2022-09-21 21:59:07 +08:00
656f5f112c fix(ftp): nil pointer dereference (close #1722) 2022-09-20 22:23:22 +08:00
9181861f47 fix: illegal files are not displayed (close #1729) 2022-09-20 20:14:38 +08:00
1ab73e0742 feat: add lanzou driver 2022-09-20 15:29:40 +08:00
57686d9df1 fix(189): file size missed 2022-09-19 19:35:07 +08:00
ca177cc3b9 fix: set default mimetype to empty string (close #1710) 2022-09-19 18:58:40 +08:00
d8dc8d8623 fix: dir duplicate creation (close #1687) 2022-09-19 13:43:23 +08:00
5548ab62ac fix: write does not take effect on the current dir (close #1711) 2022-09-19 13:35:37 +08:00
d6d82c3138 fix: page crashes if ipa name contains chinese (close #1712) 2022-09-19 13:33:23 +08:00
2185839236 chore: safe base64 decode ipa name 2022-09-18 20:17:24 +08:00
24d58f278a fix: don't use cache if no objs 2022-09-18 18:38:47 +08:00
f80be96cf9 chore: replace sep _ with @ of ipa name 2022-09-18 16:53:39 +08:00
6c89c6c8ae fix: aria2 download magnet link (close #1665) 2022-09-18 16:07:32 +08:00
b74b55fa4a feat: support custom bundle-identifier by filename 2022-09-17 21:33:39 +08:00
09564102e7 fix(aliyundrive): rapid upload empty file (close #1699) 2022-09-17 19:39:19 +08:00
d436a6e676 fix: use base64 encode for ipa install 2022-09-17 17:06:08 +08:00
bec3a327a7 fix: hide objs if only virtual files 2022-09-17 15:31:30 +08:00
d329df70f3 fix: failed create record if use mysql (close #1690) 2022-09-16 22:21:43 +08:00
1af9f4061e fix(s3): remove folder recursively 2022-09-16 21:25:55 +08:00
0d012f85cb feat: Add thunderExpert priority video url switch 2022-09-15 22:50:27 +08:00
e3b213c398 feat: add ca-certificates for docker (fix: #1679) 2022-09-15 18:56:30 +08:00
d9f0603271 fix: copy folder between two storage (fix #1670) 2022-09-15 17:58:32 +08:00
86a625cb40 fix: set CHARSET to utf8mb4 if use mysql 2022-09-15 17:14:03 +08:00
f22232de5d chore: baidu_photo rename only duplicate folders 2022-09-15 09:25:20 +08:00
7ad3748a46 feat: update cache after remove instead of clear 2022-09-14 20:28:52 +08:00
66b2562d03 fix: allow force root while fetch dirs (close #1671) 2022-09-14 19:57:39 +08:00
b197322cd8 fix: type of file with name uppercase 2022-09-14 15:14:04 +08:00
9e5ef974a7 fix: send on closed channel 2022-09-14 15:13:02 +08:00
08a001fbd1 feat: add a start func for external calls (#1628) 2022-09-13 20:12:57 +08:00
54ae6dce0b fix(fs/get): rawURL if use proxy (close #1664) 2022-09-13 20:02:57 +08:00
a90ef201c7 fix(189pc,baidu_photo,thunder): single link limit multithreading 2022-09-13 18:44:07 +08:00
2de0da87fa fix: infinite loop if new multi-level folder (close #1661) 2022-09-13 18:34:04 +08:00
53e08e75fe fix(189pc,baidu_photo): source file not closed 2022-09-12 22:45:30 +08:00
6b5236f52e feat: add baidu_photo driver 2022-09-12 17:10:02 +08:00
78e34f0d9f fix: log error if err != nil (close #1651) 2022-09-12 17:01:06 +08:00
6aedd0f425 fix: trim slash suffix of sign 2022-09-11 19:39:24 +08:00
5ff0d850d7 feat(aliyundrive): add doc and video preview api 2022-09-11 19:12:54 +08:00
cd73e34ccc chore: optional other interface 2022-09-11 18:40:19 +08:00
107462e42e chore: change default pdf viewer address 2022-09-11 18:27:28 +08:00
e6c2d22700 workflow: update docs address [skip ci] 2022-09-11 17:17:47 +08:00
889ddcef7e feat(baidu): update upload progress 2022-09-11 17:09:48 +08:00
68a6a0c40e fix(aliyundrive): upload empty file 2022-09-11 17:04:05 +08:00
969018db37 fix: is the root folder required (close #1633) 2022-09-11 16:23:46 +08:00
fba1471ec4 docs: add thunder in storage list [skip ci] 2022-09-11 15:26:47 +08:00
8b72ac7f80 chore: rename xunlei to thunder 2022-09-11 14:30:17 +08:00
77a6aa487b chore: cancel sign if no password 2022-09-11 14:14:14 +08:00
fd99c2197b fix: remove relative path check 2022-09-11 14:05:13 +08:00
9c91f062b9 fix(189pc): some minor problems 2022-09-11 13:18:29 +08:00
537ca030b2 chore: fix xunlei some minor problems 2022-09-11 13:09:36 +08:00
b00dcdec0d docs: Create CODE_OF_CONDUCT.md [skip ci] 2022-09-10 22:23:05 +08:00
57bcd376b4 fix(webdav): incorrect href if base_path isn't root (close #1629) 2022-09-10 19:27:34 +08:00
8d4d8648c6 ci: fetch dev version of alist-web 2022-09-10 19:05:02 +08:00
35d177b67b feat: add xunlei driver 2022-09-10 17:40:30 +08:00
113 changed files with 4925 additions and 540 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,13 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://alist.nn.ci/guide/sponsor.html']

View File

@ -12,9 +12,10 @@ body:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I have read the [documentation](https://alist.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure it's due to `alist` and not something else(such as `Dependencies` or `Operational`).
- label: I'm sure I'm using the latest version
- type: input
id: version
attributes:

View File

@ -7,7 +7,7 @@ body:
label: Please make sure of the following things
description: You may select more than one, even select all.
options:
- label: I have read the [documentation](https://alist-doc.nn.ci).
- label: I have read the [documentation](https://alist.nn.ci).
- label: I'm sure there are no duplicate issues or discussions.
- label: I'm sure this feature is not implemented.
- label: I'm sure it's a reasonable and popular requirement.

View File

@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.18]
go-version: [1.19]
name: Build
runs-on: ${{ matrix.platform }}
steps:

View File

@ -3,6 +3,7 @@ name: Close inactive
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-inactive:

View File

@ -3,6 +3,7 @@ name: Close need info
on:
schedule:
- cron: "0 0 */7 * *"
workflow_dispatch:
jobs:
close-need-info:

View File

@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest]
go-version: [1.18]
go-version: [1.19]
name: Release
runs-on: ${{ matrix.platform }}
steps:

128
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
i@nn.ci.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

View File

@ -7,7 +7,7 @@
Prerequisites:
- [git](https://nodejs.org/zh-cn/)
- [Go 1.18+](https://golang.org/doc/install)
- [Go 1.19+](https://golang.org/doc/install)
- [gcc](https://gcc.gnu.org/)
- [nodejs](https://nodejs.org/)

View File

@ -10,5 +10,6 @@ LABEL MAINTAINER="i@nn.ci"
VOLUME /opt/alist/data/
WORKDIR /opt/alist/
COPY --from=builder /app/bin/alist ./
RUN apk add ca-certificates
EXPOSE 5244
CMD [ "./alist", "server", "--no-prefix" ]

View File

@ -1,35 +1,45 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storage, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -51,6 +61,12 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [YandexDisk](https://disk.yandex.com/)
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -99,4 +115,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,35 +1,45 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md)
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -51,6 +61,12 @@
- [x] [Yandex.Disk](https://disk.yandex.com/)
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
- [x] [阿里云盘分享](https://www.aliyundrive.com/)
- [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -99,4 +115,4 @@
---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@QQ群](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -12,7 +12,8 @@ else
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi
echo "build version: $gitTag"
echo "backend version: $version"
echo "frontend version: $webVersion"
ldflags="\
-w -s \
@ -25,11 +26,11 @@ ldflags="\
"
FetchWebDev() {
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/main -o web-dist-main.tar.gz
tar -zxvf web-dist-main.tar.gz
curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -o web-dist-dev.tar.gz
tar -zxvf web-dist-dev.tar.gz
rm -rf public/dist
mv -f web-dist-main/dist public
rm -rf web-dist-main web-dist-main.tar.gz
mv -f web-dist-dev/dist public
rm -rf web-dist-dev web-dist-dev.tar.gz
}
FetchWebRelease() {

View File

@ -61,7 +61,7 @@ the address is defined in config file`,
<-quit
utils.Log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
utils.Log.Fatal("Server Shutdown:", err)
@ -69,7 +69,7 @@ the address is defined in config file`,
// catching ctx.Done(). timeout of 3 seconds.
select {
case <-ctx.Done():
utils.Log.Println("timeout of 3 seconds.")
utils.Log.Println("timeout of 1 seconds.")
}
utils.Log.Println("Server exiting")
},
@ -88,3 +88,12 @@ func init() {
// is called directly, e.g.:
// serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
// OutAlistInit 暴露用于外部启动server的函数
func OutAlistInit() {
var (
cmd *cobra.Command
args []string
)
serverCmd.Run(cmd, args)
}

View File

@ -164,7 +164,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
"operation": true,
"fileTrashInfoList": []File{f},
}
_, err := d.request("https://www.123pan.com/a/api/file/trash", http.MethodPost, func(req *resty.Request) {
_, err := d.request("https://www.123pan.com/b/api/file/trash", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err
@ -257,8 +257,4 @@ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *Pan123) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Pan123)(nil)

View File

@ -319,8 +319,4 @@ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return nil
}
func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Yun139)(nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -208,8 +207,4 @@ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return d.newUpload(dstDir, stream, up)
}
func (d *Cloud189) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Cloud189)(nil)

View File

@ -215,6 +215,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
ID: strconv.FormatInt(file.Id, 10),
Name: file.Name,
Modified: lastOpTime,
Size: file.Size,
},
Thumbnail: model.Thumbnail{Thumbnail: file.Icon.SmallUrl},
})

View File

@ -3,14 +3,11 @@ package _189pc
import (
"context"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -134,16 +131,17 @@ func (y *Yun189PC) Link(ctx context.Context, file model.Obj, args model.LinkArgs
"User-Agent": []string{base.UserAgent},
},
}
// 获取链接有效时常
strs := regexp.MustCompile(`(?i)expire[^=]*=([0-9]*)`).FindStringSubmatch(downloadUrl.URL)
if len(strs) == 2 {
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
if err == nil {
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
like.Expiration = &expired
/*
// 获取链接有效时常
strs := regexp.MustCompile(`(?i)expire[^=]*=([0-9]*)`).FindStringSubmatch(downloadUrl.URL)
if len(strs) == 2 {
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
if err == nil {
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
like.Expiration = &expired
}
}
}
*/
return like, nil
}
@ -278,7 +276,3 @@ func (y *Yun189PC) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
}
return y.CommonUpload(ctx, dstDir, stream, up)
}
func (y *Yun189PC) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}

View File

@ -78,8 +78,7 @@ func timestamp() int64 {
}
func MustParseTime(str string) *time.Time {
loc, _ := time.LoadLocation("Asia/Shanghai")
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", str, loc)
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
return &lastOpTime
}

View File

@ -6,10 +6,10 @@ import (
)
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
VCode string `json:"validate_code"`
RootFolderID string `json:"root_folder_id"`
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
VCode string `json:"validate_code"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
@ -17,10 +17,6 @@ type Addition struct {
RapidUpload bool `json:"rapid_upload"`
}
func (a Addition) GetRootId() string {
return a.RootFolderID
}
var config = driver.Config{
Name: "189CloudPC",
DefaultRoot: "-11",

View File

@ -140,13 +140,7 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
fullUrl += "/listFiles.action"
res := make([]model.Obj, 0, 130)
for pageNum := 1; pageNum < 100; pageNum++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
for pageNum := 1; ; pageNum++ {
var resp Cloud189FilesResp
_, err := y.get(fullUrl, func(r *resty.Request) {
r.SetContext(ctx)
@ -514,7 +508,6 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
file.SetReadCloser(tempFile)
const DEFAULT int64 = 10485760
count := int(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
@ -532,14 +525,16 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
}
silceMd5.Reset()
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), file, DEFAULT); err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5), tempFile, DEFAULT); err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
md5Byte := silceMd5.Sum(nil)
silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte)))
silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte)))
}
file.GetReadCloser().(*os.File).Seek(0, io.SeekStart)
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil)))
sliceMd5Hex := fileMd5Hex
@ -600,7 +595,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
SetContext(ctx).
SetQueryParams(clientSuffix()).
SetHeaders(ParseHttpHeader(uploadData.RequestHeader)).
SetBody(io.LimitReader(file, DEFAULT)).
SetBody(io.LimitReader(tempFile, DEFAULT)).
Put(uploadData.RequestURL)
if err != nil {
return err

View File

@ -234,7 +234,10 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
buf := make([]byte, 8)
r, _ := new(big.Int).SetString(utils.GetMD5Encode(d.AccessToken)[:16], 16)
i := new(big.Int).SetInt64(file.GetSize())
o := r.Mod(r, i)
o := new(big.Int).SetInt64(0)
if file.GetSize() > 0 {
o = r.Mod(r, i)
}
n, _ := io.NewSectionReader(tempFile, o.Int64(), 8).Read(buf[:8])
reqBody["proof_code"] = base64.StdEncoding.EncodeToString(buf[:n])
@ -264,7 +267,9 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return err
}
res.Body.Close()
up(i * 100 / count)
if count > 0 {
up(i * 100 / count)
}
}
var resp2 base.Json
_, err, e = d.request("https://api.aliyundrive.com/v2/file/complete", http.MethodPost, func(req *resty.Request) {
@ -284,7 +289,29 @@ func (d *AliDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
}
func (d *AliDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
var resp base.Json
var url string
data := base.Json{
"drive_id": d.DriveId,
"file_id": args.Obj.GetID(),
}
switch args.Method {
case "doc_preview":
url = "https://api.aliyundrive.com/v2/file/get_office_preview_url"
data["access_token"] = d.AccessToken
case "video_preview":
url = "https://api.aliyundrive.com/v2/file/get_video_preview_play_info"
data["category"] = "live_transcoding"
default:
return nil, errs.NotSupport
}
_, err, _ := d.request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
var _ driver.Driver = (*AliDrive)(nil)

View File

@ -40,6 +40,7 @@ func fileToObj(f File) *model.ObjThumb {
Modified: f.UpdatedAt,
IsFolder: f.Type == "folder",
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
}

View File

@ -0,0 +1,178 @@
package aliyundrive_share
import (
"context"
"errors"
"net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/cron"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
)
type AliyundriveShare struct {
model.Storage
Addition
AccessToken string
ShareToken string
DriveId string
cron *cron.Cron
}
func (d *AliyundriveShare) Config() driver.Config {
return config
}
func (d *AliyundriveShare) GetAddition() driver.Additional {
return d.Addition
}
func (d *AliyundriveShare) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
err = d.refreshToken()
if err != nil {
return err
}
err = d.getShareToken()
if err != nil {
return err
}
d.cron = cron.NewCron(time.Hour * 2)
d.cron.Do(func() {
err := d.refreshToken()
if err != nil {
log.Errorf("%+v", err)
}
})
return nil
}
func (d *AliyundriveShare) Drop(ctx context.Context) error {
if d.cron != nil {
d.cron.Stop()
}
return nil
}
func (d *AliyundriveShare) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src File) (model.Obj, error) {
return fileToObj(src), nil
})
}
//func (d *AliyundriveShare) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AliyundriveShare) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
data := base.Json{
"drive_id": d.DriveId,
"file_id": file.GetID(),
"expire_sec": 14400,
}
var e ErrorResp
res, err := base.RestyClient.R().
SetError(&e).SetBody(data).
SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
Post("https://api.aliyundrive.com/v2/file/get_download_url")
if err != nil {
return nil, err
}
var u string
if e.Code != "" {
if e.Code == "AccessTokenInvalid" {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.Link(ctx, file, args)
} else if e.Code == "ForbiddenNoPermission.File" {
data = utils.MergeMap(data, base.Json{
// Only ten minutes valid
"expire_sec": 600,
"share_id": d.ShareId,
})
var resp ShareLinkResp
var e2 ErrorResp
_, err = base.RestyClient.R().
SetError(&e2).SetBody(data).SetResult(&resp).
SetHeader("content-type", "application/json").
SetHeader("Authorization", "Bearer\t"+d.AccessToken).
SetHeader("x-share-token", d.ShareToken).
Post("https://api.aliyundrive.com/v2/file/get_share_link_download_url")
if err != nil {
return nil, err
}
if e2.Code != "" {
if e2.Code == "AccessTokenInvalid" || e2.Code == "ShareLinkTokenInvalid" {
err = d.getShareToken()
if err != nil {
return nil, err
}
return d.Link(ctx, file, args)
} else {
return nil, errors.New(e2.Code + ":" + e2.Message)
}
} else {
u = resp.DownloadUrl
}
} else {
return nil, errors.New(e.Code + ":" + e.Message)
}
} else {
u = utils.Json.Get(res.Body(), "url").ToString()
}
return &model.Link{
Header: http.Header{
"Referer": []string{"https://www.aliyundrive.com/"},
},
URL: u,
}, nil
}
func (d *AliyundriveShare) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
// TODO create folder
return errs.NotSupport
}
func (d *AliyundriveShare) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO move obj
return errs.NotSupport
}
func (d *AliyundriveShare) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
// TODO rename obj
return errs.NotSupport
}
func (d *AliyundriveShare) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
// TODO copy obj
return errs.NotSupport
}
func (d *AliyundriveShare) Remove(ctx context.Context, obj model.Obj) error {
// TODO remove obj
return errs.NotSupport
}
func (d *AliyundriveShare) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// TODO upload file
return errs.NotSupport
}
var _ driver.Driver = (*AliyundriveShare)(nil)

View File

@ -0,0 +1,29 @@
package aliyundrive_share
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
ShareId string `json:"share_id" required:"true"`
SharePwd string `json:"share_pwd"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"name,size,updated_at,created_at"`
OrderDirection string `json:"order_direction" type:"select" options:"ASC,DESC"`
}
var config = driver.Config{
Name: "AliyundriveShare",
LocalSort: false,
OnlyProxy: false,
NoUpload: true,
DefaultRoot: "root",
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &AliyundriveShare{}
})
}

View File

@ -0,0 +1,57 @@
package aliyundrive_share
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type ErrorResp struct {
Code string `json:"code"`
Message string `json:"message"`
}
type ShareTokenResp struct {
ShareToken string `json:"share_token"`
ExpireTime time.Time `json:"expire_time"`
ExpiresIn int `json:"expires_in"`
}
type ListResp struct {
Items []File `json:"items"`
NextMarker string `json:"next_marker"`
PunishedFileCount int `json:"punished_file_count"`
}
type File struct {
DriveId string `json:"drive_id"`
DomainId string `json:"domain_id"`
FileId string `json:"file_id"`
ShareId string `json:"share_id"`
Name string `json:"name"`
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ParentFileId string `json:"parent_file_id"`
Size int64 `json:"size"`
Thumbnail string `json:"thumbnail"`
}
func fileToObj(f File) *model.ObjThumb {
return &model.ObjThumb{
Object: model.Object{
ID: f.FileId,
Name: f.Name,
Size: f.Size,
Modified: f.UpdatedAt,
IsFolder: f.Type == "folder",
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
}
type ShareLinkResp struct {
DownloadUrl string `json:"download_url"`
Url string `json:"url"`
Thumbnail string `json:"thumbnail"`
}

View File

@ -0,0 +1,99 @@
package aliyundrive_share
import (
"errors"
"fmt"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/op"
log "github.com/sirupsen/logrus"
)
func (d *AliyundriveShare) refreshToken() error {
url := "https://auth.aliyundrive.com/v2/account/token"
var resp base.TokenResp
var e ErrorResp
_, err := base.RestyClient.R().
SetBody(base.Json{"refresh_token": d.RefreshToken, "grant_type": "refresh_token"}).
SetResult(&resp).
SetError(&e).
Post(url)
if err != nil {
return err
}
if e.Code != "" {
return fmt.Errorf("failed to refresh token: %s", e.Message)
}
d.RefreshToken, d.AccessToken = resp.RefreshToken, resp.AccessToken
op.MustSaveDriverStorage(d)
return nil
}
// do others that not defined in Driver interface
func (d *AliyundriveShare) getShareToken() error {
data := base.Json{
"share_id": d.ShareId,
}
if d.SharePwd != "" {
data["share_pwd"] = d.SharePwd
}
var e ErrorResp
var resp ShareTokenResp
_, err := base.RestyClient.R().
SetResult(&resp).SetError(&e).SetBody(data).
Post("https://api.aliyundrive.com/v2/share_link/get_share_token")
if err != nil {
return err
}
if e.Code != "" {
return errors.New(e.Message)
}
d.ShareToken = resp.ShareToken
return nil
}
func (d *AliyundriveShare) getFiles(fileId string) ([]File, error) {
files := make([]File, 0)
data := base.Json{
"image_thumbnail_process": "image/resize,w_160/format,jpeg",
"image_url_process": "image/resize,w_1920/format,jpeg",
"limit": 100,
"order_by": d.OrderBy,
"order_direction": d.OrderDirection,
"parent_file_id": fileId,
"share_id": d.ShareId,
"video_thumbnail_process": "video/snapshot,t_1000,f_jpg,ar_auto,w_300",
"marker": "first",
}
for data["marker"] != "" {
if data["marker"] == "first" {
data["marker"] = ""
}
var e ErrorResp
var resp ListResp
res, err := base.RestyClient.R().
SetHeader("x-share-token", d.ShareToken).
SetResult(&resp).SetError(&e).SetBody(data).
Post("https://api.aliyundrive.com/adrive/v3/file/list")
if err != nil {
return nil, err
}
log.Debugf("aliyundrive share get files: %s", res.String())
if e.Code != "" {
if e.Code == "AccessTokenInvalid" || e.Code == "ShareLinkTokenInvalid" {
err = d.getShareToken()
if err != nil {
return nil, err
}
return d.getFiles(fileId)
}
return nil, errors.New(e.Message)
}
data["marker"] = resp.NextMarker
files = append(files, resp.Items...)
}
if len(files) > 0 && d.DriveId == "" {
d.DriveId = files[0].DriveId
}
return files, nil
}

View File

@ -6,17 +6,23 @@ import (
_ "github.com/alist-org/alist/v3/drivers/189"
_ "github.com/alist-org/alist/v3/drivers/189pc"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/quark"
_ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/sftp"
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/uss"
_ "github.com/alist-org/alist/v3/drivers/virtual"
_ "github.com/alist-org/alist/v3/drivers/webdav"

View File

@ -15,7 +15,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
@ -202,7 +201,7 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
"uploadid": precreateResp.Uploadid,
}
left = stream.GetSize()
for _, partseq := range precreateResp.BlockList {
for i, partseq := range precreateResp.BlockList {
byteSize := Default
var byteData []byte
if left < Default {
@ -223,13 +222,12 @@ func (d *BaiduNetdisk) Put(ctx context.Context, dstDir model.Obj, stream model.F
return err
}
log.Debugln(res.String())
if len(precreateResp.BlockList) > 0 {
up(i * 100 / len(precreateResp.BlockList))
}
}
_, err = d.create(path, stream.GetSize(), 0, precreateResp.Uploadid, block_list_str)
return err
}
func (d *BaiduNetdisk) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*BaiduNetdisk)(nil)

View File

@ -0,0 +1,282 @@
package baiduphoto
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"math"
"os"
"regexp"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type BaiduPhoto struct {
model.Storage
Addition
AccessToken string
}
func (d *BaiduPhoto) Config() driver.Config {
return config
}
func (d *BaiduPhoto) GetAddition() driver.Additional {
return d.Addition
}
func (d *BaiduPhoto) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.refreshToken()
}
func (d *BaiduPhoto) Drop(ctx context.Context) error {
return nil
}
func (d *BaiduPhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
var objs []model.Obj
var err error
if IsRoot(dir) {
var albums []Album
if d.ShowType != "root_only_file" {
albums, err = d.GetAllAlbum(ctx)
if err != nil {
return nil, err
}
}
var files []File
if d.ShowType != "root_only_album" {
files, err = d.GetAllFile(ctx)
if err != nil {
return nil, err
}
}
alubmName := make(map[string]int)
objs, _ = utils.SliceConvert(albums, func(album Album) (model.Obj, error) {
i := alubmName[album.GetName()]
if i != 0 {
alubmName[album.GetName()]++
album.Title = fmt.Sprintf("%s(%d)", album.Title, i)
}
alubmName[album.GetName()]++
return &album, nil
})
for i := 0; i < len(files); i++ {
objs = append(objs, &files[i])
}
} else if IsAlbum(dir) || IsAlbumRoot(dir) {
var files []AlbumFile
files, err = d.GetAllAlbumFile(ctx, splitID(dir.GetID())[0], "")
if err != nil {
return nil, err
}
objs = make([]model.Obj, 0, len(files))
for i := 0; i < len(files); i++ {
objs = append(objs, &files[i])
}
}
return objs, nil
}
func (d *BaiduPhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if IsAlbumFile(file) {
return d.linkAlbum(ctx, file, args)
} else if IsFile(file) {
return d.linkFile(ctx, file, args)
}
return nil, errs.NotFile
}
func (d *BaiduPhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if IsRoot(parentDir) {
code := regexp.MustCompile(`(?i)join:([\S]*)`).FindStringSubmatch(dirName)
if len(code) > 1 {
return d.JoinAlbum(ctx, code[1])
}
return d.CreateAlbum(ctx, dirName)
}
return errs.NotSupport
}
func (d *BaiduPhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if IsFile(srcObj) {
if IsAlbum(dstDir) {
//rootfile -> album
e := splitID(dstDir.GetID())
return d.AddAlbumFile(ctx, e[0], e[1], srcObj.GetID())
}
} else if IsAlbumFile(srcObj) {
if IsRoot(dstDir) {
//albumfile -> root
e := splitID(srcObj.GetID())
_, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID())
return err
} else if IsAlbum(dstDir) {
// albumfile -> root -> album
e := splitID(srcObj.GetID())
file, err := d.CopyAlbumFile(ctx, e[1], e[2], e[3], srcObj.GetID())
if err != nil {
return err
}
e = splitID(dstDir.GetID())
return d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(file.Fsid))
}
}
return errs.NotSupport
}
func (d *BaiduPhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
// 仅支持相册之间移动
if IsAlbumFile(srcObj) && IsAlbum(dstDir) {
err := d.Copy(ctx, srcObj, dstDir)
if err != nil {
return err
}
e := splitID(srcObj.GetID())
return d.DeleteAlbumFile(ctx, e[1], e[2], srcObj.GetID())
}
return errs.NotSupport
}
func (d *BaiduPhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
// 仅支持相册改名
if IsAlbum(srcObj) {
e := splitID(srcObj.GetID())
return d.SetAlbumName(ctx, e[0], e[1], newName)
}
return errs.NotSupport
}
func (d *BaiduPhoto) Remove(ctx context.Context, obj model.Obj) error {
e := splitID(obj.GetID())
if IsFile(obj) {
return d.DeleteFile(ctx, e[0])
} else if IsAlbum(obj) {
return d.DeleteAlbum(ctx, e[0], e[1])
} else if IsAlbumFile(obj) {
return d.DeleteAlbumFile(ctx, e[1], e[2], obj.GetID())
}
return errs.NotSupport
}
func (d *BaiduPhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
// 需要获取完整文件md5,必须支持 io.Seek
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
// 计算需要的数据
const DEFAULT = 1 << 22
const SliceSize = 1 << 18
count := int(math.Ceil(float64(stream.GetSize()) / float64(DEFAULT)))
sliceMD5List := make([]string, 0, count)
fileMd5 := md5.New()
sliceMd5 := md5.New()
sliceMd52 := md5.New()
slicemd52Write := utils.LimitWriter(sliceMd52, SliceSize)
for i := 1; i <= count; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
_, err := io.CopyN(io.MultiWriter(fileMd5, sliceMd5, slicemd52Write), tempFile, DEFAULT)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
sliceMD5List = append(sliceMD5List, hex.EncodeToString(sliceMd5.Sum(nil)))
sliceMd5.Reset()
}
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return err
}
content_md5 := hex.EncodeToString(fileMd5.Sum(nil))
slice_md5 := hex.EncodeToString(sliceMd52.Sum(nil))
// 开始执行上传
params := map[string]string{
"autoinit": "1",
"isdir": "0",
"rtype": "1",
"ctype": "11",
"path": stream.GetName(),
"size": fmt.Sprint(stream.GetSize()),
"slice-md5": slice_md5,
"content-md5": content_md5,
"block_list": MustString(utils.Json.MarshalToString(sliceMD5List)),
}
// 预上传
var precreateResp PrecreateResp
_, err = d.Post(FILE_API_URL_V1+"/precreate", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(params)
}, &precreateResp)
if err != nil {
return err
}
switch precreateResp.ReturnType {
case 1: // 上传文件
uploadParams := map[string]string{
"method": "upload",
"path": params["path"],
"uploadid": precreateResp.UploadID,
}
for i := 0; i < count; i++ {
uploadParams["partseq"] = fmt.Sprint(i)
_, err = d.Post("https://c3.pcs.baidu.com/rest/2.0/pcs/superfile2", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(uploadParams)
r.SetFileReader("file", stream.GetName(), io.LimitReader(tempFile, DEFAULT))
}, nil)
if err != nil {
return err
}
up(i * 100 / count)
}
fallthrough
case 2: // 创建文件
params["uploadid"] = precreateResp.UploadID
_, err = d.Post(FILE_API_URL_V1+"/create", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(params)
}, &precreateResp)
if err != nil {
return err
}
fallthrough
case 3: // 增加到相册
if IsAlbum(dstDir) || IsAlbumRoot(dstDir) {
e := splitID(dstDir.GetID())
err = d.AddAlbumFile(ctx, e[0], e[1], fmt.Sprint(precreateResp.Data.FsID))
if err != nil {
return err
}
}
}
return nil
}
var _ driver.Driver = (*BaiduPhoto)(nil)

107
drivers/baidu_photo/help.go Normal file
View File

@ -0,0 +1,107 @@
package baiduphoto
import (
"fmt"
"math"
"math/rand"
"regexp"
"strings"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
//Tid生成
func getTid() string {
return fmt.Sprintf("3%d%.0f", time.Now().Unix(), math.Floor(9000000*rand.Float64()+1000000))
}
// 检查名称
func checkName(name string) bool {
return len(name) <= 20 && regexp.MustCompile("[\u4e00-\u9fa5A-Za-z0-9_-]").MatchString(name)
}
func toTime(t int64) *time.Time {
tm := time.Unix(t, 0)
return &tm
}
func fsidsFormat(ids ...string) string {
var buf []string
for _, id := range ids {
e := splitID(id)
buf = append(buf, fmt.Sprintf(`{"fsid":%s,"uk":%s}`, e[0], e[3]))
}
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
}
func fsidsFormatNotUk(ids ...string) string {
var buf []string
for _, id := range ids {
buf = append(buf, fmt.Sprintf(`{"fsid":%s}`, splitID(id)[0]))
}
return fmt.Sprintf("[%s]", strings.Join(buf, ","))
}
/*
结构
{fsid} 文件
{album_id}|{tid} 相册
{fsid}|{album_id}|{tid}|{uk} 相册文件
*/
func splitID(id string) []string {
return strings.SplitN(id, "|", 4)[:4]
}
/*
结构
{fsid} 文件
{album_id}|{tid} 相册
{fsid}|{album_id}|{tid}|{uk} 相册文件
*/
func joinID(ids ...interface{}) string {
idsStr := make([]string, 0, len(ids))
for _, id := range ids {
idsStr = append(idsStr, fmt.Sprint(id))
}
return strings.Join(idsStr, "|")
}
func getFileName(path string) string {
return path[strings.LastIndex(path, "/")+1:]
}
// 相册
func IsAlbum(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "album"
}
// 根目录
func IsRoot(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "" && obj.GetID() == ""
}
// 以相册为根目录
func IsAlbumRoot(obj model.Obj) bool {
return obj.IsDir() && obj.GetPath() == "" && obj.GetID() != ""
}
// 根文件
func IsFile(obj model.Obj) bool {
return !obj.IsDir() && obj.GetPath() == "file"
}
// 相册文件
func IsAlbumFile(obj model.Obj) bool {
return !obj.IsDir() && obj.GetPath() == "albumfile"
}
func MustString(str string, err error) string {
return str
}

View File

@ -0,0 +1,30 @@
package baiduphoto
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
RefreshToken string `json:"refresh_token" required:"true"`
ShowType string `json:"show_type" type:"select" options:"root,root_only_album,root_only_file" default:"root"`
AlbumID string `json:"album_id"`
//AlbumPassword string `json:"album_password"`
ClientID string `json:"client_id" required:"true" default:"iYCeC9g08h5vuP9UqvPHKKSVrKFXGa1v"`
ClientSecret string `json:"client_secret" required:"true" default:"jXiFMOPVPCWlO2M5CwWQzffpNPaGTRBG"`
}
func (a Addition) GetRootId() string {
return a.AlbumID
}
var config = driver.Config{
Name: "BaiduPhoto",
LocalSort: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &BaiduPhoto{}
})
}

View File

@ -0,0 +1,169 @@
package baiduphoto
import (
"fmt"
"time"
)
type TokenErrResp struct {
ErrorDescription string `json:"error_description"`
ErrorMsg string `json:"error"`
}
func (e *TokenErrResp) Error() string {
return fmt.Sprint(e.ErrorMsg, " : ", e.ErrorDescription)
}
type Erron struct {
Errno int `json:"errno"`
RequestID int `json:"request_id"`
}
type Page struct {
HasMore int `json:"has_more"`
Cursor string `json:"cursor"`
}
func (p Page) HasNextPage() bool {
return p.HasMore == 1
}
type (
FileListResp struct {
Page
List []File `json:"list"`
}
File struct {
Fsid int64 `json:"fsid"` // 文件ID
Path string `json:"path"` // 文件路径
Size int64 `json:"size"`
Ctime int64 `json:"ctime"` // 创建时间 s
Mtime int64 `json:"mtime"` // 修改时间 s
Thumburl []string `json:"thumburl"`
parseTime *time.Time
}
)
func (c *File) GetSize() int64 { return c.Size }
func (c *File) GetName() string { return getFileName(c.Path) }
func (c *File) ModTime() time.Time {
if c.parseTime == nil {
c.parseTime = toTime(c.Mtime)
}
return *c.parseTime
}
func (c *File) IsDir() bool { return false }
func (c *File) GetID() string { return joinID(c.Fsid) }
func (c *File) GetPath() string { return "file" }
func (c *File) Thumb() string {
if len(c.Thumburl) > 0 {
return c.Thumburl[0]
}
return ""
}
/*相册部分*/
type (
AlbumListResp struct {
Page
List []Album `json:"list"`
Reset int64 `json:"reset"`
TotalCount int64 `json:"total_count"`
}
Album struct {
AlbumID string `json:"album_id"`
Tid int64 `json:"tid"`
Title string `json:"title"`
JoinTime int64 `json:"join_time"`
CreateTime int64 `json:"create_time"`
Mtime int64 `json:"mtime"`
parseTime *time.Time
}
AlbumFileListResp struct {
Page
List []AlbumFile `json:"list"`
Reset int64 `json:"reset"`
TotalCount int64 `json:"total_count"`
}
AlbumFile struct {
File
AlbumID string `json:"album_id"`
Tid int64 `json:"tid"`
Uk int64 `json:"uk"`
}
)
func (a *Album) GetSize() int64 { return 0 }
func (a *Album) GetName() string { return fmt.Sprint(a.Title) }
func (a *Album) ModTime() time.Time {
if a.parseTime == nil {
a.parseTime = toTime(a.Mtime)
}
return *a.parseTime
}
func (a *Album) IsDir() bool { return true }
func (a *Album) GetID() string { return joinID(a.AlbumID, a.Tid) }
func (a *Album) GetPath() string { return "album" }
func (af *AlbumFile) GetID() string { return joinID(af.Fsid, af.AlbumID, af.Tid, af.Uk) }
func (c *AlbumFile) GetPath() string { return "albumfile" }
type (
CopyFileResp struct {
List []CopyFile `json:"list"`
}
CopyFile struct {
FromFsid int64 `json:"from_fsid"` // 源ID
Fsid int64 `json:"fsid"` // 目标ID
Path string `json:"path"`
ShootTime int `json:"shoot_time"`
}
)
/*上传部分*/
type (
UploadFile struct {
FsID int64 `json:"fs_id"`
Size int64 `json:"size"`
Md5 string `json:"md5"`
ServerFilename string `json:"server_filename"`
Path string `json:"path"`
Ctime int `json:"ctime"`
Mtime int `json:"mtime"`
Isdir int `json:"isdir"`
Category int `json:"category"`
ServerMd5 string `json:"server_md5"`
ShootTime int `json:"shoot_time"`
}
CreateFileResp struct {
Data UploadFile `json:"data"`
}
PrecreateResp struct {
ReturnType int `json:"return_type"` //存在返回2 不存在返回1 已经保存3
//存在返回
CreateFileResp
//不存在返回
Path string `json:"path"`
UploadID string `json:"uploadid"`
Blocklist []int64 `json:"block_list"`
}
)
type InviteResp struct {
Pdata struct {
// 邀请码
InviteCode string `json:"invite_code"`
// 有效时间
ExpireTime int `json:"expire_time"`
ShareID string `json:"share_id"`
} `json:"pdata"`
}

View File

@ -0,0 +1,378 @@
package baiduphoto
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
const (
API_URL = "https://photo.baidu.com/youai"
ALBUM_API_URL = API_URL + "/album/v1"
FILE_API_URL_V1 = API_URL + "/file/v1"
FILE_API_URL_V2 = API_URL + "/file/v2"
)
var (
ErrNotSupportName = errors.New("only chinese and english, numbers and underscores are supported, and the length is no more than 20")
)
func (p *BaiduPhoto) Request(furl string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R().
SetQueryParam("access_token", p.AccessToken)
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, furl)
if err != nil {
return nil, err
}
erron := utils.Json.Get(res.Body(), "errno").ToInt()
switch erron {
case 0:
break
case 50805:
return nil, fmt.Errorf("you have joined album")
case 50820:
return nil, fmt.Errorf("no shared albums found")
case -6:
if err = p.refreshToken(); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("errno: %d, refer to https://photo.baidu.com/union/doc", erron)
}
return res.Body(), nil
}
func (p *BaiduPhoto) refreshToken() error {
u := "https://openapi.baidu.com/oauth/2.0/token"
var resp base.TokenResp
var e TokenErrResp
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).SetQueryParams(map[string]string{
"grant_type": "refresh_token",
"refresh_token": p.RefreshToken,
"client_id": p.ClientID,
"client_secret": p.ClientSecret,
}).Get(u)
if err != nil {
return err
}
if e.ErrorMsg != "" {
return &e
}
if resp.RefreshToken == "" {
return errs.EmptyToken
}
p.AccessToken, p.RefreshToken = resp.AccessToken, resp.RefreshToken
op.MustSaveDriverStorage(p)
return nil
}
func (p *BaiduPhoto) Get(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodGet, callback, resp)
}
func (p *BaiduPhoto) Post(furl string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return p.Request(furl, http.MethodPost, callback, resp)
}
// 获取所有文件
func (p *BaiduPhoto) GetAllFile(ctx context.Context) (files []File, err error) {
var cursor string
for {
var resp FileListResp
_, err = p.Get(FILE_API_URL_V1+"/list", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"need_thumbnail": "1",
"need_filter_hidden": "0",
"cursor": cursor,
})
}, &resp)
if err != nil {
return
}
files = append(files, resp.List...)
if !resp.HasNextPage() {
return
}
cursor = resp.Cursor
}
}
// 删除根文件
func (p *BaiduPhoto) DeleteFile(ctx context.Context, fileIDs ...string) error {
_, err := p.Get(FILE_API_URL_V1+"/delete", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"fsid_list": fmt.Sprintf("[%s]", strings.Join(fileIDs, ",")),
})
}, nil)
return err
}
// 获取所有相册
func (p *BaiduPhoto) GetAllAlbum(ctx context.Context) (albums []Album, err error) {
var cursor string
for {
var resp AlbumListResp
_, err = p.Get(ALBUM_API_URL+"/list", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"need_amount": "1",
"limit": "100",
"cursor": cursor,
})
}, &resp)
if err != nil {
return
}
if albums == nil {
albums = make([]Album, 0, resp.TotalCount)
}
cursor = resp.Cursor
albums = append(albums, resp.List...)
if !resp.HasNextPage() {
return
}
}
}
// 获取相册中所有文件
func (p *BaiduPhoto) GetAllAlbumFile(ctx context.Context, albumID, passwd string) (files []AlbumFile, err error) {
var cursor string
for {
var resp AlbumFileListResp
_, err = p.Get(ALBUM_API_URL+"/listfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"album_id": albumID,
"need_amount": "1",
"limit": "1000",
"passwd": passwd,
"cursor": cursor,
})
}, &resp)
if err != nil {
return
}
if files == nil {
files = make([]AlbumFile, 0, resp.TotalCount)
}
cursor = resp.Cursor
files = append(files, resp.List...)
if !resp.HasNextPage() {
return
}
}
}
// 创建相册
func (p *BaiduPhoto) CreateAlbum(ctx context.Context, name string) error {
if !checkName(name) {
return ErrNotSupportName
}
_, err := p.Post(ALBUM_API_URL+"/create", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"title": name,
"tid": getTid(),
"source": "0",
})
}, nil)
return err
}
// 相册改名
func (p *BaiduPhoto) SetAlbumName(ctx context.Context, albumID, tID, name string) error {
if !checkName(name) {
return ErrNotSupportName
}
_, err := p.Post(ALBUM_API_URL+"/settitle", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"title": name,
"album_id": albumID,
"tid": tID,
})
}, nil)
return err
}
// 删除相册
func (p *BaiduPhoto) DeleteAlbum(ctx context.Context, albumID, tID string) error {
_, err := p.Post(ALBUM_API_URL+"/delete", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"delete_origin_image": "0", // 是否删除原图 0 不删除 1 删除
})
}, nil)
return err
}
// 删除相册文件
func (p *BaiduPhoto) DeleteAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error {
_, err := p.Post(ALBUM_API_URL+"/delfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"list": fsidsFormat(fileIDs...),
"del_origin": "0", // 是否删除原图 0 不删除 1 删除
})
}, nil)
return err
}
// 增加相册文件
func (p *BaiduPhoto) AddAlbumFile(ctx context.Context, albumID, tID string, fileIDs ...string) error {
_, err := p.Get(ALBUM_API_URL+"/addfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"album_id": albumID,
"tid": tID,
"list": fsidsFormatNotUk(fileIDs...),
})
}, nil)
return err
}
// 保存相册文件为根文件
func (p *BaiduPhoto) CopyAlbumFile(ctx context.Context, albumID, tID, uk string, fileID ...string) (*CopyFile, error) {
var resp CopyFileResp
_, err := p.Post(ALBUM_API_URL+"/copyfile", func(r *resty.Request) {
r.SetContext(ctx)
r.SetFormData(map[string]string{
"album_id": albumID,
"tid": tID,
"uk": uk,
"list": fsidsFormatNotUk(fileID...),
})
r.SetResult(&resp)
}, nil)
if err != nil {
return nil, err
}
return &resp.List[0], nil
}
// 加入相册
func (p *BaiduPhoto) JoinAlbum(ctx context.Context, code string) error {
var resp InviteResp
_, err := p.Get(ALBUM_API_URL+"/querypcode", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"pcode": code,
"web": "1",
})
}, &resp)
if err != nil {
return err
}
_, err = p.Get(ALBUM_API_URL+"/join", func(req *resty.Request) {
req.SetContext(ctx)
req.SetQueryParams(map[string]string{
"invite_code": resp.Pdata.InviteCode,
})
}, nil)
return err
}
func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
headers := map[string]string{
"User-Agent": base.UserAgent,
}
if args.Header.Get("User-Agent") != "" {
headers["User-Agent"] = args.Header.Get("User-Agent")
}
if !utils.IsLocalIPAddr(args.IP) {
headers["X-Forwarded-For"] = args.IP
}
e := splitID(file.GetID())
res, err := base.NoRedirectClient.R().
SetContext(ctx).
SetHeaders(headers).
SetQueryParams(map[string]string{
"access_token": d.AccessToken,
"fsid": e[0],
"album_id": e[1],
"tid": e[2],
"uk": e[3],
}).
Head(ALBUM_API_URL + "/download")
if err != nil {
return nil, err
}
//exp := 8 * time.Hour
link := &model.Link{
URL: res.Header().Get("location"),
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}
return link, nil
}
func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
headers := map[string]string{
"User-Agent": base.UserAgent,
}
if args.Header.Get("User-Agent") != "" {
headers["User-Agent"] = args.Header.Get("User-Agent")
}
if !utils.IsLocalIPAddr(args.IP) {
headers["X-Forwarded-For"] = args.IP
}
var downloadUrl struct {
Dlink string `json:"dlink"`
}
_, err := d.Get(FILE_API_URL_V2+"/download", func(r *resty.Request) {
r.SetContext(ctx)
r.SetHeaders(headers)
r.SetQueryParams(map[string]string{
"fsid": splitID(file.GetID())[0],
})
}, &downloadUrl)
if err != nil {
return nil, err
}
//exp := 8 * time.Hour
link := &model.Link{
URL: downloadUrl.Dlink,
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}
return link, nil
}

View File

@ -127,8 +127,4 @@ func (d *FTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
return d.conn.Stor(stdpath.Join(dstDir.GetPath(), stream.GetName()), stream)
}
func (d *FTP) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*FTP)(nil)

View File

@ -19,5 +19,6 @@ func (d *FTP) login() error {
if err != nil {
return err
}
d.conn = conn
return nil
}

View File

@ -141,8 +141,4 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return err
}
func (d *GoogleDrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*GoogleDrive)(nil)

View File

@ -0,0 +1,170 @@
package google_photo
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
type GooglePhoto struct {
model.Storage
Addition
AccessToken string
}
func (d *GooglePhoto) Config() driver.Config {
return config
}
func (d *GooglePhoto) GetAddition() driver.Additional {
return d.Addition
}
func (d *GooglePhoto) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.refreshToken()
}
func (d *GooglePhoto) Drop(ctx context.Context) error {
return nil
}
func (d *GooglePhoto) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles()
if err != nil {
return nil, err
}
return utils.SliceConvert(files, func(src MediaItem) (model.Obj, error) {
return fileToObj(src), nil
})
}
//func (d *GooglePhoto) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *GooglePhoto) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
f, err := d.getFile(file.GetID())
if err != nil {
return nil, err
}
if strings.Contains(f.MimeType, "image/") {
return &model.Link{
URL: f.BaseURL + "=d",
}, nil
} else if strings.Contains(f.MimeType, "video/") {
return &model.Link{
URL: f.BaseURL + "=dv",
}, nil
}
return &model.Link{}, nil
}
func (d *GooglePhoto) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotSupport
}
func (d *GooglePhoto) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotSupport
}
func (d *GooglePhoto) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotSupport
}
func (d *GooglePhoto) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var e Error
// Create resumable upload url
postHeaders := map[string]string{
"Authorization": "Bearer " + d.AccessToken,
"Content-type": "application/octet-stream",
"X-Goog-Upload-Command": "start",
"X-Goog-Upload-Content-Type": stream.GetMimetype(),
"X-Goog-Upload-Protocol": "resumable",
"X-Goog-Upload-Raw-Size": strconv.FormatInt(stream.GetSize(), 10),
}
url := "https://photoslibrary.googleapis.com/v1/uploads"
res, err := base.NoRedirectClient.R().SetHeaders(postHeaders).
SetError(&e).
Post(url)
if err != nil {
return err
}
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = d.refreshToken()
if err != nil {
return err
}
return d.Put(ctx, dstDir, stream, up)
}
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
//Upload to the Google Photo
postUrl := res.Header().Get("X-Goog-Upload-URL")
//chunkSize := res.Header().Get("X-Goog-Upload-Chunk-Granularity")
postHeaders = map[string]string{
"X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": "0",
}
resp, err := d.request(postUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(stream.GetReadCloser())
}, nil, postHeaders)
if err != nil {
return err
}
//Create MediaItem
createItemUrl := "https://photoslibrary.googleapis.com/v1/mediaItems:batchCreate"
postHeaders = map[string]string{
"X-Goog-Upload-Command": "upload, finalize",
"X-Goog-Upload-Offset": "0",
}
data := base.Json{
"newMediaItems": []base.Json{
{
"description": "item-description",
"simpleMediaItem": base.Json{
"fileName": stream.GetName(),
"uploadToken": string(resp),
},
},
},
}
_, err = d.request(createItemUrl, http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil, postHeaders)
return err
}
var _ driver.Driver = (*GooglePhoto)(nil)

View File

@ -0,0 +1,29 @@
package google_photo
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootID
RefreshToken string `json:"refresh_token" required:"true"`
ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"`
ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"`
}
var config = driver.Config{
Name: "GooglePhoto",
OnlyProxy: true,
DefaultRoot: "root",
NoUpload: true,
LocalSort: true,
}
func New() driver.Driver {
return &GooglePhoto{}
}
func init() {
op.RegisterDriver(config, New)
}

View File

@ -0,0 +1,69 @@
package google_photo
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type TokenError struct {
Error string `json:"error"`
ErrorDescription string `json:"error_description"`
}
type Files struct {
NextPageToken string `json:"nextPageToken"`
MediaItems []MediaItem `json:"mediaItems"`
}
type MediaItem struct {
Id string `json:"id"`
BaseURL string `json:"baseUrl"`
MimeType string `json:"mimeType"`
FileName string `json:"filename"`
MediaMetadata MediaMetadata `json:"mediaMetadata"`
}
type MediaMetadata struct {
CreationTime time.Time `json:"creationTime"`
Width string `json:"width"`
Height string `json:"height"`
Photo Photo `json:"photo,omitempty"`
Video Video `json:"video,omitempty"`
}
type Photo struct {
}
type Video struct {
}
func fileToObj(f MediaItem) *model.ObjThumb {
//size, _ := strconv.ParseInt(f.Size, 10, 64)
return &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.FileName,
Size: 0,
Modified: f.MediaMetadata.CreationTime,
IsFolder: false,
},
Thumbnail: model.Thumbnail{
Thumbnail: f.BaseURL + "=w100-h100-c",
},
}
}
type Error struct {
Error struct {
Errors []struct {
Domain string `json:"domain"`
Reason string `json:"reason"`
Message string `json:"message"`
LocationType string `json:"location_type"`
Location string `json:"location"`
}
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}

View File

@ -0,0 +1,105 @@
package google_photo
import (
"fmt"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
// do others that not defined in Driver interface
func (d *GooglePhoto) refreshToken() error {
url := "https://www.googleapis.com/oauth2/v4/token"
var resp base.TokenResp
var e TokenError
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).
SetFormData(map[string]string{
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
"refresh_token": d.RefreshToken,
"grant_type": "refresh_token",
}).Post(url)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf(e.Error)
}
d.AccessToken = resp.AccessToken
return nil
}
func (d *GooglePhoto) request(url string, method string, callback base.ReqCallback, resp interface{}, headers map[string]string) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
if headers != nil {
req.SetHeaders(headers)
}
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Error
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp, headers)
}
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
return res.Body(), nil
}
func (d *GooglePhoto) getFiles() ([]MediaItem, error) {
pageToken := "first"
res := make([]MediaItem, 0)
for pageToken != "" {
if pageToken == "first" {
pageToken = ""
}
var resp Files
query := map[string]string{
"fields": "mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",
"pageSize": "100",
"pageToken": pageToken,
}
_, err := d.request("https://photoslibrary.googleapis.com/v1/mediaItems", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return nil, err
}
pageToken = resp.NextPageToken
res = append(res, resp.MediaItems...)
}
return res, nil
}
func (d *GooglePhoto) getFile(id string) (MediaItem, error) {
var resp MediaItem
query := map[string]string{
"fields": "baseUrl,mimeType",
}
_, err := d.request(fmt.Sprintf("https://photoslibrary.googleapis.com/v1/mediaItems/%s", id), http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return resp, err
}
return resp, nil
}

171
drivers/lanzou/driver.go Normal file
View File

@ -0,0 +1,171 @@
package lanzou
import (
"context"
"net/http"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
var upClient = base.NewRestyClient().SetTimeout(120 * time.Second)
type LanZou struct {
Addition
model.Storage
}
func (d *LanZou) Config() driver.Config {
return config
}
func (d *LanZou) GetAddition() driver.Additional {
return d.Addition
}
func (d *LanZou) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
if d.IsCookie() {
if d.RootFolderID == "" {
d.RootFolderID = "-1"
}
}
return nil
}
func (d *LanZou) Drop(ctx context.Context) error {
return nil
}
// 获取的大小和时间不准确
func (d *LanZou) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if d.IsCookie() {
return d.GetFiles(ctx, dir.GetID())
} else {
return d.GetFileOrFolderByShareUrl(ctx, dir.GetID(), d.SharePassword)
}
}
func (d *LanZou) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downID := file.GetID()
pwd := d.SharePassword
if d.IsCookie() {
share, err := d.getFileShareUrlByID(ctx, file.GetID())
if err != nil {
return nil, err
}
downID = share.FID
pwd = share.Pwd
}
fileInfo, err := d.getFilesByShareUrl(ctx, downID, pwd, nil)
if err != nil {
return nil, err
}
return &model.Link{
URL: fileInfo.Url,
Header: http.Header{
"User-Agent": []string{base.UserAgent},
},
}, nil
}
func (d *LanZou) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if d.IsCookie() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "2",
"parent_id": parentDir.GetID(),
"folder_name": dirName,
"folder_description": "",
})
}, nil)
return err
}
return errs.NotImplement
}
func (d *LanZou) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if d.IsCookie() {
if !srcObj.IsDir() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "20",
"folder_id": dstDir.GetID(),
"file_id": srcObj.GetID(),
})
}, nil)
return err
}
}
return errs.NotImplement
}
func (d *LanZou) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if d.IsCookie() {
if !srcObj.IsDir() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "46",
"file_id": srcObj.GetID(),
"file_name": newName,
"type": "2",
})
}, nil)
return err
}
}
return errs.NotImplement
}
func (d *LanZou) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *LanZou) Remove(ctx context.Context, obj model.Obj) error {
if d.IsCookie() {
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
if obj.IsDir() {
req.SetFormData(map[string]string{
"task": "3",
"folder_id": obj.GetID(),
})
} else {
req.SetFormData(map[string]string{
"task": "6",
"file_id": obj.GetID(),
})
}
}, nil)
return err
}
return errs.NotImplement
}
func (d *LanZou) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if d.IsCookie() {
_, err := d._post(d.BaseUrl+"/fileup.php", func(req *resty.Request) {
req.SetFormData(map[string]string{
"task": "1",
"id": "WU_FILE_0",
"name": stream.GetName(),
"folder_id": dstDir.GetID(),
}).SetFileReader("upload_file", stream.GetName(), stream)
}, nil, true)
return err
}
return errs.NotImplement
}

165
drivers/lanzou/help.go Normal file
View File

@ -0,0 +1,165 @@
package lanzou
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"unicode"
)
const DAY time.Duration = 84600000000000
var timeSplitReg = regexp.MustCompile("([0-9.]*)\\s*([\u4e00-\u9fa5]+)")
func MustParseTime(str string) time.Time {
lastOpTime, err := time.ParseInLocation("2006-01-02 -07", str+" +08", time.Local)
if err != nil {
strs := timeSplitReg.FindStringSubmatch(str)
lastOpTime = time.Now()
if len(strs) == 3 {
i, _ := strconv.ParseInt(strs[1], 10, 64)
ti := time.Duration(-i)
switch strs[2] {
case "秒前":
lastOpTime = lastOpTime.Add(time.Second * ti)
case "分钟前":
lastOpTime = lastOpTime.Add(time.Minute * ti)
case "小时前":
lastOpTime = lastOpTime.Add(time.Hour * ti)
case "天前":
lastOpTime = lastOpTime.Add(DAY * ti)
case "昨天":
lastOpTime = lastOpTime.Add(-DAY)
case "前天":
lastOpTime = lastOpTime.Add(-DAY * 2)
}
}
}
return lastOpTime
}
var sizeSplitReg = regexp.MustCompile(`(?i)([0-9.]+)\s*([bkm]+)`)
func SizeStrToInt64(size string) int64 {
strs := sizeSplitReg.FindStringSubmatch(size)
if len(strs) < 3 {
return 0
}
s, _ := strconv.ParseFloat(strs[1], 64)
switch strings.ToUpper(strs[2]) {
case "B":
return int64(s)
case "K":
return int64(s * (1 << 10))
case "M":
return int64(s * (1 << 20))
}
return 0
}
// 移除注释
func RemoveNotes(html []byte) []byte {
return regexp.MustCompile(`<!--.*?-->|//.*|/\*.*?\*/`).ReplaceAll(html, []byte{})
}
var findAcwScV2Reg = regexp.MustCompile(`arg1='([0-9A-Z]+)'`)
// 在页面被过多访问或其他情况下有时候会先返回一个加密的页面其执行计算出一个acw_sc__v2后放入页面后再重新访问页面才能获得正常页面
// 若该页面进行了js加密则进行解密计算acw_sc__v2并加入cookie
func CalcAcwScV2(html string) (string, error) {
acwScV2s := findAcwScV2Reg.FindStringSubmatch(html)
if len(acwScV2s) != 2 {
return "", fmt.Errorf("无法匹配acw_sc__v2")
}
return HexXor(Unbox(acwScV2s[1]), "3000176000856006061501533003690027800375"), nil
}
func Unbox(hex string) string {
var box = []int{6, 28, 34, 31, 33, 18, 30, 23, 9, 8, 19, 38, 17, 24, 0, 5, 32, 21, 10, 22, 25, 14, 15, 3, 16, 27, 13, 35, 2, 29, 11, 26, 4, 36, 1, 39, 37, 7, 20, 12}
var newBox = make([]byte, len(hex))
for i := 0; i < len(box); i++ {
j := box[i]
if len(newBox) > j {
newBox[j] = hex[i]
}
}
return string(newBox)
}
func HexXor(hex1, hex2 string) string {
out := bytes.NewBuffer(make([]byte, len(hex1)))
for i := 0; i < len(hex1) && i < len(hex2); i += 2 {
v1, _ := strconv.ParseInt(hex1[i:i+2], 16, 64)
v2, _ := strconv.ParseInt(hex2[i:i+2], 16, 64)
out.WriteString(strconv.FormatInt(v1^v2, 16))
}
return out.String()
}
var findDataReg = regexp.MustCompile(`data[:\s]+({[^}]+})`) // 查找json
var findKVReg = regexp.MustCompile(`'(.+?)':('?([^' },]*)'?)`) // 拆分kv
// 根据key查询js变量
func findJSVarFunc(key, data string) string {
values := regexp.MustCompile(`var ` + key + ` = '(.+?)';`).FindStringSubmatch(data)
if len(values) == 0 {
return ""
}
return values[1]
}
// 解析html中的JSON
func htmlJsonToMap(html string) (map[string]string, error) {
datas := findDataReg.FindStringSubmatch(html)
if len(datas) != 2 {
return nil, fmt.Errorf("not find data")
}
return jsonToMap(datas[1], html), nil
}
func jsonToMap(data, html string) map[string]string {
var param = make(map[string]string)
kvs := findKVReg.FindAllStringSubmatch(data, -1)
for _, kv := range kvs {
k, v := kv[1], kv[3]
if v == "" || strings.Contains(kv[2], "'") || IsNumber(kv[2]) {
param[k] = v
} else {
param[k] = findJSVarFunc(v, html)
}
}
return param
}
func IsNumber(str string) bool {
for _, s := range str {
if !unicode.IsDigit(s) {
return false
}
}
return true
}
var findFromReg = regexp.MustCompile(`data : '(.+?)'`) // 查找from字符串
// 解析html中的from
func htmlFormToMap(html string) (map[string]string, error) {
froms := findFromReg.FindStringSubmatch(html)
if len(froms) != 2 {
return nil, fmt.Errorf("not find file sgin")
}
return fromToMap(froms[1]), nil
}
func fromToMap(from string) map[string]string {
var param = make(map[string]string)
for _, kv := range strings.Split(from, "&") {
kv := strings.SplitN(kv, "=", 2)[:2]
param[kv[0]] = kv[1]
}
return param
}

31
drivers/lanzou/meta.go Normal file
View File

@ -0,0 +1,31 @@
package lanzou
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Type string `json:"type" type:"select" options:"cookie,url" default:"cookie"`
Cookie string `json:"cookie" required:"true" help:"about 15 days valid"`
driver.RootID
SharePassword string `json:"share_password"`
BaseUrl string `json:"baseUrl" required:"true" default:"https://pc.woozooo.com"`
ShareUrl string `json:"shareUrl" required:"true" default:"https://pan.lanzouo.com"`
}
func (a *Addition) IsCookie() bool {
return a.Type == "cookie"
}
var config = driver.Config{
Name: "Lanzou",
LocalSort: true,
DefaultRoot: "-1",
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &LanZou{}
})
}

131
drivers/lanzou/types.go Normal file
View File

@ -0,0 +1,131 @@
package lanzou
import (
"fmt"
"time"
"github.com/alist-org/alist/v3/internal/model"
)
type FilesOrFoldersResp struct {
Text []FileOrFolder `json:"text"`
}
type FileOrFolder struct {
Name string `json:"name"`
//Onof string `json:"onof"` // 是否存在提取码
//IsLock string `json:"is_lock"`
//IsCopyright int `json:"is_copyright"`
// 文件通用
ID string `json:"id"`
NameAll string `json:"name_all"`
Size string `json:"size"`
Time string `json:"time"`
//Icon string `json:"icon"`
//Downs string `json:"downs"`
//Filelock string `json:"filelock"`
//IsBakdownload int `json:"is_bakdownload"`
//Bakdownload string `json:"bakdownload"`
//IsDes int `json:"is_des"` // 是否存在描述
//IsIco int `json:"is_ico"`
// 文件夹
FolID string `json:"fol_id"`
//Folderlock string `json:"folderlock"`
//FolderDes string `json:"folder_des"`
}
func (f *FileOrFolder) isFloder() bool {
return f.FolID != ""
}
func (f *FileOrFolder) ToObj() model.Obj {
obj := &model.Object{}
if f.isFloder() {
obj.ID = f.FolID
obj.Name = f.Name
obj.Modified = time.Now()
obj.IsFolder = true
} else {
obj.ID = f.ID
obj.Name = f.NameAll
obj.Modified = MustParseTime(f.Time)
obj.Size = SizeStrToInt64(f.Size)
}
return obj
}
type FileShareResp struct {
Info FileShare `json:"info"`
}
type FileShare struct {
Pwd string `json:"pwd"`
Onof string `json:"onof"`
Taoc string `json:"taoc"`
IsNewd string `json:"is_newd"`
// 文件
FID string `json:"f_id"`
// 文件夹
NewUrl string `json:"new_url"`
Name string `json:"name"`
Des string `json:"des"`
}
type FileOrFolderByShareUrlResp struct {
Text []FileOrFolderByShareUrl `json:"text"`
}
type FileOrFolderByShareUrl struct {
ID string `json:"id"`
NameAll string `json:"name_all"`
Size string `json:"size"`
Time string `json:"time"`
Duan string `json:"duan"`
//Icon string `json:"icon"`
//PIco int `json:"p_ico"`
//T int `json:"t"`
IsFloder bool
}
func (f *FileOrFolderByShareUrl) ToObj() model.Obj {
return &model.Object{
ID: f.ID,
Name: f.NameAll,
Size: SizeStrToInt64(f.Size),
Modified: MustParseTime(f.Time),
IsFolder: f.IsFloder,
}
}
type FileShareInfoAndUrlResp[T string | int] struct {
Dom string `json:"dom"`
URL string `json:"url"`
Inf T `json:"inf"`
}
func (u *FileShareInfoAndUrlResp[T]) GetBaseUrl() string {
return fmt.Sprint(u.Dom, "/file")
}
func (u *FileShareInfoAndUrlResp[T]) GetDownloadUrl() string {
return fmt.Sprint(u.GetBaseUrl(), "/", u.URL)
}
// 通过分享链接获取文件信息和下载链接
type FileInfoAndUrlByShareUrl struct {
ID string
Name string
Size string
Time string
Url string
}
func (f *FileInfoAndUrlByShareUrl) ToObj() model.Obj {
return &model.Object{
ID: f.ID,
Name: f.Name,
Size: SizeStrToInt64(f.Size),
Modified: MustParseTime(f.Time),
}
}

416
drivers/lanzou/util.go Normal file
View File

@ -0,0 +1,416 @@
package lanzou
import (
"context"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
func (d *LanZou) get(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d.request(url, http.MethodGet, callback, false)
}
func (d *LanZou) post(url string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
return d._post(url, callback, resp, false)
}
func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{}, up bool) ([]byte, error) {
data, err := d.request(url, http.MethodPost, callback, up)
if err != nil {
return nil, err
}
switch utils.Json.Get(data, "zt").ToInt() {
case 1, 2, 4:
if resp != nil {
// 返回类型不统一,忽略错误
utils.Json.Unmarshal(data, resp)
}
return data, nil
default:
info := utils.Json.Get(data, "inf").ToString()
if info == "" {
info = utils.Json.Get(data, "info").ToString()
}
return nil, fmt.Errorf(info)
}
}
func (d *LanZou) request(url string, method string, callback base.ReqCallback, up bool) ([]byte, error) {
var req *resty.Request
if up {
req = upClient.R()
} else {
req = base.RestyClient.R()
}
req.SetHeaders(map[string]string{
"Referer": "https://pc.woozooo.com",
})
if d.Cookie != "" {
req.SetHeader("cookie", d.Cookie)
}
if callback != nil {
callback(req)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
return res.Body(), err
}
/*
通过cookie获取数据
*/
// 获取文件和文件夹,获取到的文件大小、更改时间不可信
func (d *LanZou) GetFiles(ctx context.Context, folderID string) ([]model.Obj, error) {
folders, err := d.getFolders(ctx, folderID)
if err != nil {
return nil, err
}
files, err := d.getFiles(ctx, folderID)
if err != nil {
return nil, err
}
objs := make([]model.Obj, 0, len(folders)+len(files))
for _, folder := range folders {
objs = append(objs, folder.ToObj())
}
for _, file := range files {
objs = append(objs, file.ToObj())
}
return objs, nil
}
// 通过ID获取文件夹
func (d *LanZou) getFolders(ctx context.Context, folderID string) ([]FileOrFolder, error) {
var resp FilesOrFoldersResp
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "47",
"folder_id": folderID,
})
}, &resp)
if err != nil {
return nil, err
}
return resp.Text, nil
}
// 通过ID获取文件
func (d *LanZou) getFiles(ctx context.Context, folderID string) ([]FileOrFolder, error) {
files := make([]FileOrFolder, 0)
for pg := 1; ; pg++ {
var resp FilesOrFoldersResp
_, err := d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "5",
"folder_id": folderID,
"pg": strconv.Itoa(pg),
})
}, &resp)
if err != nil {
return nil, err
}
if len(resp.Text) == 0 {
break
}
files = append(files, resp.Text...)
}
return files, nil
}
// 通过ID获取文件夹分享地址
func (d *LanZou) getFolderShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) {
var resp FileShareResp
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "18",
"file_id": fileID,
})
}, &resp)
if err != nil {
return
}
share = resp.Info
return
}
// 通过ID获取文件分享地址
func (d *LanZou) getFileShareUrlByID(ctx context.Context, fileID string) (share FileShare, err error) {
var resp FileShareResp
_, err = d.post(d.BaseUrl+"/doupload.php", func(req *resty.Request) {
req.SetContext(ctx)
req.SetFormData(map[string]string{
"task": "22",
"file_id": fileID,
})
}, &resp)
if err != nil {
return
}
share = resp.Info
return
}
/*
通过分享链接获取数据
*/
// 判断类容
var isFileReg = regexp.MustCompile(`class="fileinfo"|id="file"|文件描述`)
var isFolderReg = regexp.MustCompile(`id="infos"`)
// 获取文件文件夹基础信息
var nameFindReg = regexp.MustCompile(`<title>(.+?) - 蓝奏云</title>|id="filenajax">(.+?)</div>|var filename = '(.+?)';|<div style="font-size.+?>([^<>].+?)</div>|<div class="filethetext".+?>([^<>]+?)</div>`)
var sizeFindReg = regexp.MustCompile(`(?i)大小\W*([0-9.]+\s*[bkm]+)`)
var timeFindReg = regexp.MustCompile(`\d+\s*[秒天分小][钟时]?前|[昨前]天|\d{4}-\d{2}-\d{2}`)
var findSubFolaerReg = regexp.MustCompile(`(folderlink|mbxfolder).+href="/(.+?)"(.+filename")?>(.+?)<`) // 查找分享文件夹子文件夹ID和名称
// 获取关键数据
var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
// 通过分享链接获取文件或文件夹,如果是文件则会返回下载链接
func (d *LanZou) GetFileOrFolderByShareUrl(ctx context.Context, downID, pwd string) ([]model.Obj, error) {
pageData, err := d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
if err != nil {
return nil, err
}
pageData = RemoveNotes(pageData)
var objs []model.Obj
if !isFileReg.Match(pageData) {
files, err := d.getFolderByShareUrl(ctx, downID, pwd, pageData)
if err != nil {
return nil, err
}
objs = make([]model.Obj, 0, len(files))
for _, file := range files {
objs = append(objs, file.ToObj())
}
} else {
file, err := d.getFilesByShareUrl(ctx, downID, pwd, pageData)
if err != nil {
return nil, err
}
objs = []model.Obj{file.ToObj()}
}
return objs, nil
}
// 通过分享链接获取文件(下载链接也使用此方法)
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L440
func (d *LanZou) getFilesByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) (file FileInfoAndUrlByShareUrl, err error) {
if firstPageData == nil {
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
if err != nil {
return
}
firstPageData = RemoveNotes(firstPageData)
}
firstPageDataStr := string(firstPageData)
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
var vs string
if vs, err = CalcAcwScV2(firstPageDataStr); err != nil {
return
}
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
req.SetCookie(&http.Cookie{
Name: "acw_sc__v2",
Value: vs,
})
req.SetContext(ctx)
}, nil)
if err != nil {
return
}
firstPageData = RemoveNotes(firstPageData)
firstPageDataStr = string(firstPageData)
}
var (
param map[string]string
downloadUrl string
baseUrl string
)
// 需要密码
if strings.Contains(firstPageDataStr, "pwdload") || strings.Contains(firstPageDataStr, "passwddiv") {
param, err = htmlFormToMap(firstPageDataStr)
if err != nil {
return
}
param["p"] = pwd
var resp FileShareInfoAndUrlResp[string]
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp)
if err != nil {
return
}
file.Name = resp.Inf
baseUrl = resp.GetBaseUrl()
downloadUrl = resp.GetDownloadUrl()
} else {
urlpaths := findDownPageParamReg.FindStringSubmatch(firstPageDataStr)
if len(urlpaths) != 2 {
err = fmt.Errorf("not find file page param")
return
}
var nextPageData []byte
nextPageData, err = d.get(fmt.Sprint(d.ShareUrl, urlpaths[1]), func(req *resty.Request) { req.SetContext(ctx) }, nil)
if err != nil {
return
}
nextPageData = RemoveNotes(nextPageData)
nextPageDataStr := string(nextPageData)
param, err = htmlJsonToMap(nextPageDataStr)
if err != nil {
return
}
var resp FileShareInfoAndUrlResp[int]
_, err = d.post(d.ShareUrl+"/ajaxm.php", func(req *resty.Request) { req.SetFormData(param).SetContext(ctx) }, &resp)
if err != nil {
return
}
baseUrl = resp.GetBaseUrl()
downloadUrl = resp.GetDownloadUrl()
names := nameFindReg.FindStringSubmatch(firstPageDataStr)
if len(names) > 1 {
for _, name := range names[1:] {
if name != "" {
file.Name = name
break
}
}
}
}
sizes := sizeFindReg.FindStringSubmatch(firstPageDataStr)
if len(sizes) == 2 {
file.Size = sizes[1]
}
file.ID = downID
file.Time = timeFindReg.FindString(firstPageDataStr)
// 重定向获取真实链接
res, err := base.NoRedirectClient.R().SetHeaders(map[string]string{
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
}).SetContext(ctx).Get(downloadUrl)
if err != nil {
return
}
file.Url = res.Header().Get("location")
// 触发验证
rPageDataStr := res.String()
if res.StatusCode() != 302 && strings.Contains(rPageDataStr, "网络异常") {
param, err = htmlJsonToMap(rPageDataStr)
if err != nil {
return
}
param["el"] = "2"
time.Sleep(time.Second * 2)
// 通过验证获取直连
var rUrl struct {
Url string `json:"url"`
}
_, err = d.post(fmt.Sprint(baseUrl, "/ajax.php"), func(req *resty.Request) { req.SetContext(ctx).SetFormData(param) }, &rUrl)
if err != nil {
return
}
file.Url = rUrl.Url
}
return
}
// 通过分享链接获取文件夹
// 参考 https://github.com/zaxtyson/LanZouCloud-API/blob/ab2e9ec715d1919bf432210fc16b91c6775fbb99/lanzou/api/core.py#L1089
func (d *LanZou) getFolderByShareUrl(ctx context.Context, downID, pwd string, firstPageData []byte) ([]FileOrFolderByShareUrl, error) {
if firstPageData == nil {
var err error
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) { req.SetContext(ctx) }, nil)
if err != nil {
return nil, err
}
firstPageData = RemoveNotes(firstPageData)
}
firstPageDataStr := string(firstPageData)
//
if strings.Contains(firstPageDataStr, "acw_sc__v2") {
vs, err := CalcAcwScV2(firstPageDataStr)
if err != nil {
return nil, err
}
firstPageData, err = d.get(fmt.Sprint(d.ShareUrl, "/", downID), func(req *resty.Request) {
req.SetCookie(&http.Cookie{
Name: "acw_sc__v2",
Value: vs,
})
req.SetContext(ctx)
}, nil)
if err != nil {
return nil, err
}
firstPageData = RemoveNotes(firstPageData)
firstPageDataStr = string(firstPageData)
}
from, err := htmlJsonToMap(firstPageDataStr)
if err != nil {
return nil, err
}
from["pwd"] = pwd
files := make([]FileOrFolderByShareUrl, 0)
// vip获取文件夹
floders := findSubFolaerReg.FindAllStringSubmatch(firstPageDataStr, -1)
for _, floder := range floders {
if len(floder) == 5 {
files = append(files, FileOrFolderByShareUrl{
ID: floder[2],
NameAll: floder[4],
IsFloder: true,
})
}
}
for page := 1; ; page++ {
from["pg"] = strconv.Itoa(page)
var resp FileOrFolderByShareUrlResp
_, err := d.post(d.ShareUrl+"/filemoreajax.php", func(req *resty.Request) { req.SetFormData(from).SetContext(ctx) }, &resp)
if err != nil {
return nil, err
}
files = append(files, resp.Text...)
if len(resp.Text) == 0 {
break
}
time.Sleep(time.Millisecond * 600)
}
return files, nil
}

View File

@ -175,9 +175,9 @@ func (d *Local) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
dstPath := filepath.Join(dstDir.GetPath(), srcObj.GetName())
var err error
if srcObj.IsDir() {
err = copyDir(srcPath, dstPath)
err = utils.CopyDir(srcPath, dstPath)
} else {
err = copyFile(srcPath, dstPath)
err = utils.CopyFile(srcPath, dstPath)
}
if err != nil {
return err
@ -217,8 +217,4 @@ func (d *Local) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return nil
}
func (d *Local) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Local)(nil)

View File

@ -1,67 +1 @@
package local
import (
"fmt"
"io"
"io/ioutil"
"os"
"path"
)
// copyFile File copies a single file from src to dst
func copyFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = os.Create(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
// copyDir Dir copies a whole directory recursively
func copyDir(src string, dst string) error {
var err error
var fds []os.FileInfo
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = ioutil.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
if fd.IsDir() {
if err = copyDir(srcfp, dstfp); err != nil {
fmt.Println(err)
}
} else {
if err = copyFile(srcfp, dstfp); err != nil {
fmt.Println(err)
}
}
}
return nil
}

View File

@ -13,7 +13,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
@ -221,8 +220,4 @@ func (d *MediaTrack) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
func (d *MediaTrack) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*MediaTrack)(nil)

192
drivers/mega/driver.go Normal file
View File

@ -0,0 +1,192 @@
package mega
import (
"context"
"errors"
"fmt"
"io"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/chanio"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
"github.com/t3rm1n4l/go-mega"
)
type Mega struct {
model.Storage
Addition
c *mega.Mega
}
func (d *Mega) Config() driver.Config {
return config
}
func (d *Mega) GetAddition() driver.Additional {
return d.Addition
}
func (d *Mega) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
d.c = mega.New()
return d.c.Login(d.Email, d.Password)
}
func (d *Mega) Drop(ctx context.Context) error {
return nil
}
func (d *Mega) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if node, ok := dir.(*MegaNode); ok {
nodes, err := d.c.FS.GetChildren(node.Node)
if err != nil {
return nil, err
}
res := make([]model.Obj, 0)
for i := range nodes {
n := nodes[i]
if n.GetType() == mega.FILE || n.GetType() == mega.FOLDER {
res = append(res, &MegaNode{n})
}
}
return res, nil
}
log.Errorf("can't convert: %+v", dir)
return nil, fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Get(ctx context.Context, path string) (model.Obj, error) {
if path == "/" {
n := d.c.FS.GetRoot()
log.Debugf("mega root: %+v", *n)
return &MegaNode{n}, nil
}
return nil, errs.NotSupport
}
func (d *Mega) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if node, ok := file.(*MegaNode); ok {
//link, err := d.c.Link(node.Node, true)
//if err != nil {
// return nil, err
//}
//return &model.Link{URL: link}, nil
down, err := d.c.NewDownload(node.Node)
if err != nil {
return nil, err
}
//u := down.GetResourceUrl()
//u = strings.Replace(u, "http", "https", 1)
//return &model.Link{URL: u}, nil
c := chanio.New()
go func() {
defer func() {
_ = recover()
}()
log.Debugf("chunk size: %d", down.Chunks())
for id := 0; id < down.Chunks(); id++ {
chunk, err := down.DownloadChunk(id)
if err != nil {
log.Errorf("mega down: %+v", err)
return
}
log.Debugf("id: %d,len: %d", id, len(chunk))
//_, _, err = down.ChunkLocation(id)
//if err != nil {
// log.Errorf("mega down: %+v", err)
// return
//}
//_, err = c.Write(chunk)
_, err = c.Write(chunk)
}
err := c.Close()
if err != nil {
log.Errorf("mega down: %+v", err)
}
}()
return &model.Link{Data: c}, nil
}
return nil, fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if parentNode, ok := parentDir.(*MegaNode); ok {
_, err := d.c.CreateDir(dirName, parentNode.Node)
return err
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if srcNode, ok := srcObj.(*MegaNode); ok {
if dstNode, ok := dstDir.(*MegaNode); ok {
return d.c.Move(srcNode.Node, dstNode.Node)
}
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if srcNode, ok := srcObj.(*MegaNode); ok {
return d.c.Rename(srcNode.Node, newName)
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *Mega) Remove(ctx context.Context, obj model.Obj) error {
if node, ok := obj.(*MegaNode); ok {
return d.c.Delete(node.Node, false)
}
return fmt.Errorf("unable to convert dir to mega node")
}
func (d *Mega) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if dstNode, ok := dstDir.(*MegaNode); ok {
u, err := d.c.NewUpload(dstNode.Node, stream.GetName(), stream.GetSize())
if err != nil {
return err
}
for id := 0; id < u.Chunks(); id++ {
_, chkSize, err := u.ChunkLocation(id)
if err != nil {
return err
}
chunk := make([]byte, chkSize)
n, err := io.ReadFull(stream, chunk)
if err != nil && err != io.EOF {
return err
}
if n != len(chunk) {
return errors.New("chunk too short")
}
err = u.UploadChunk(id, chunk)
if err != nil {
return err
}
up(id * 100 / u.Chunks())
}
_, err = u.Finish()
return err
}
return fmt.Errorf("unable to convert dir to mega node")
}
//func (d *Mega) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Mega)(nil)

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

@ -0,0 +1,26 @@
package mega
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
//driver.RootID
Email string `json:"email" required:"true"`
Password string `json:"password" required:"true"`
}
var config = driver.Config{
Name: "Mega_nz",
LocalSort: true,
OnlyLocal: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &Mega{}
})
}

40
drivers/mega/types.go Normal file
View File

@ -0,0 +1,40 @@
package mega
import (
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/t3rm1n4l/go-mega"
)
type MegaNode struct {
*mega.Node
}
//func (m *MegaNode) GetSize() int64 {
// //TODO implement me
// panic("implement me")
//}
//
//func (m *MegaNode) GetName() string {
// //TODO implement me
// panic("implement me")
//}
func (m *MegaNode) ModTime() time.Time {
return m.GetTimeStamp()
}
func (m *MegaNode) IsDir() bool {
return m.GetType() == mega.FOLDER || m.GetType() == mega.ROOT
}
func (m *MegaNode) GetID() string {
return m.GetHash()
}
func (m *MegaNode) GetPath() string {
return ""
}
var _ model.Obj = (*MegaNode)(nil)

3
drivers/mega/util.go Normal file
View File

@ -0,0 +1,3 @@
package mega
// do others that not defined in Driver interface

View File

@ -34,6 +34,9 @@ func (d *Onedrive) Init(ctx context.Context, storage model.Storage) error {
if err != nil {
return err
}
if d.ChunkSize < 1 {
d.ChunkSize = 5
}
return d.refreshToken()
}
@ -144,8 +147,4 @@ func (d *Onedrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return err
}
func (d *Onedrive) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Onedrive)(nil)

View File

@ -7,13 +7,14 @@ import (
type Addition struct {
driver.RootPath
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de"`
Region string `json:"region" type:"select" required:"true" options:"global,cn,us,de" default:"global"`
IsSharepoint bool `json:"is_sharepoint"`
ClientID string `json:"client_id" required:"true"`
ClientSecret string `json:"client_secret" required:"true"`
RedirectUri string `json:"redirect_uri" required:"true" default:"https://tool.nn.ci/onedrive/callback"`
RefreshToken string `json:"refresh_token" required:"true"`
SiteId string `json:"site_id"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
}
var config = driver.Config{

View File

@ -42,6 +42,7 @@ var onedriveHostMap = map[string]Host{
func (d *Onedrive) GetMetaUrl(auth bool, path string) string {
host, _ := onedriveHostMap[d.Region]
path = utils.EncodePath(path, true)
if auth {
return host.Oauth
}
@ -166,7 +167,7 @@ func (d *Onedrive) upBig(ctx context.Context, dstDir model.Obj, stream model.Fil
}
uploadUrl := jsoniter.Get(res, "uploadUrl").ToString()
var finish int64 = 0
const DEFAULT = 4 * 1024 * 1024
DEFAULT := d.ChunkSize * 1024 * 1024
for finish < stream.GetSize() {
if utils.IsCanceled(ctx) {
return ctx.Err()

View File

@ -12,7 +12,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/aws/aws-sdk-go/aws"
@ -199,8 +198,4 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *PikPak) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*PikPak)(nil)

View File

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"os"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -90,6 +91,9 @@ func (d *Quark) MakeDir(ctx context.Context, parentDir model.Obj, dirName string
_, err := d.request("/file", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
if err == nil {
time.Sleep(time.Second)
}
return err
}
@ -215,8 +219,4 @@ func (d *Quark) Put(ctx context.Context, dstDir model.Obj, stream model.FileStre
return d.upFinish(pre)
}
func (d *Quark) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Quark)(nil)

View File

@ -8,7 +8,7 @@ import (
type Addition struct {
Cookie string `json:"cookie" required:"true"`
driver.RootID
OrderBy string `json:"order_by" type:"select" options:"file_type,file_name,updated_at" default:"file_name"`
OrderBy string `json:"order_by" type:"select" options:"none,file_type,file_name,updated_at" default:"none"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
}

View File

@ -62,7 +62,9 @@ func (d *Quark) GetFiles(parent string) ([]File, error) {
"pdir_fid": parent,
"_size": strconv.Itoa(size),
"_fetch_total": "1",
"_sort": "file_type:asc," + d.OrderBy + ":" + d.OrderDirection,
}
if d.OrderBy != "none" {
query["_sort"] = "file_type:asc," + d.OrderBy + ":" + d.OrderDirection
}
for {
query["_page"] = strconv.Itoa(page)

View File

@ -10,7 +10,6 @@ import (
"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/aws/aws-sdk-go/aws/session"
@ -131,13 +130,10 @@ func (d *S3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
}
func (d *S3) Remove(ctx context.Context, obj model.Obj) error {
key := getKey(obj.GetPath(), obj.IsDir())
input := &s3.DeleteObjectInput{
Bucket: &d.Bucket,
Key: &key,
if obj.IsDir() {
return d.removeDir(ctx, obj.GetPath())
}
_, err := d.client.DeleteObject(input)
return err
return d.removeFile(obj.GetPath())
}
func (d *S3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
@ -153,8 +149,4 @@ func (d *S3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreame
return err
}
func (d *S3) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*S3)(nil)

View File

@ -52,12 +52,11 @@ func getKey(path string, dir bool) string {
return path
}
var defaultPlaceholderName = ".placeholder"
// var defaultPlaceholderName = ".placeholder"
func getPlaceholderName(placeholder string) string {
if placeholder == "" {
return defaultPlaceholderName
}
//if placeholder == "" {
// return defaultPlaceholderName
//}
return placeholder
}
@ -205,3 +204,33 @@ func (d *S3) copyDir(ctx context.Context, src string, dst string) error {
}
return nil
}
func (d *S3) removeDir(ctx context.Context, src string) error {
objs, err := op.List(ctx, d, src, model.ListArgs{})
if err != nil {
return err
}
for _, obj := range objs {
cSrc := path.Join(src, obj.GetName())
if obj.IsDir() {
err = d.removeDir(ctx, cSrc)
} else {
err = d.removeFile(cSrc)
}
if err != nil {
return err
}
}
_ = d.removeFile(path.Join(src, getPlaceholderName(d.Placeholder)))
return nil
}
func (d *S3) removeFile(src string) error {
key := getKey(src, true)
input := &s3.DeleteObjectInput{
Bucket: &d.Bucket,
Key: &key,
}
_, err := d.client.DeleteObject(input)
return err
}

View File

@ -99,8 +99,4 @@ func (d *SFTP) Put(ctx context.Context, dstDir model.Obj, stream model.FileStrea
return err
}
func (d *SFTP) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*SFTP)(nil)

View File

@ -7,7 +7,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -156,8 +155,4 @@ func (d *Teambition) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return d.finishUpload(newFile, dstDir.GetID())
}
func (d *Teambition) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Teambition)(nil)

View File

@ -42,10 +42,10 @@ func (d *Template) List(ctx context.Context, dir model.Obj, args model.ListArgs)
return nil, errs.NotImplement
}
func (d *Template) Get(ctx context.Context, path string) (model.Obj, error) {
// this is optional
return nil, errs.NotImplement
}
//func (d *Template) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *Template) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// TODO return link of file
@ -82,8 +82,8 @@ func (d *Template) Put(ctx context.Context, dstDir model.Obj, stream model.FileS
return errs.NotImplement
}
func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
//func (d *Template) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*Template)(nil)

503
drivers/thunder/driver.go Normal file
View File

@ -0,0 +1,503 @@
package thunder
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"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/s3manager"
"github.com/go-resty/resty/v2"
)
type Thunder struct {
*XunLeiCommon
model.Storage
Addition
identity string
}
func (x *Thunder) Config() driver.Config {
return config
}
func (x *Thunder) GetAddition() driver.Additional {
return x.Addition
}
func (x *Thunder) Init(ctx context.Context, storage model.Storage) (err error) {
x.Storage = storage
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.Addition); err != nil {
return err
}
// 初始化所需参数
if x.XunLeiCommon == nil {
x.XunLeiCommon = &XunLeiCommon{
Common: &Common{
client: base.NewRestyClient(),
Algorithms: []string{
"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR",
"GzhNkZ8pOBsCY+7",
"v+l0ImTpG7c7/",
"e5ztohgVXNP",
"t",
"EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO",
"o7dvYgbRMOpHXxCs",
"6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+",
"kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb",
"j",
"4scKJNdd7F27Hv7tbt",
},
DeviceID: "9aa5c268e7bcfc197a9ad88e2fb330e5",
ClientID: "Xp6vsxz_7IYVw2BB",
ClientSecret: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
ClientVersion: "7.51.0.8196",
PackageName: "com.xunlei.downloadprovider",
UserAgent: "ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
},
refreshTokenFunc: func() error {
// 通过RefreshToken刷新
token, err := x.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
// 重新登录
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
op.MustSaveDriverStorage(x)
}
}
x.SetTokenResp(token)
return err
},
}
}
// 自定义验证码token
ctoekn := strings.TrimSpace(x.CaptchaToken)
if ctoekn != "" {
x.SetCaptchaToken(ctoekn)
x.CaptchaToken = ""
}
// 防止重复登录
identity := x.GetIdentity()
if x.identity != identity || !x.IsLogin() {
x.identity = identity
// 登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
}
return nil
}
func (x *Thunder) Drop(ctx context.Context) error {
return nil
}
type ThunderExpert struct {
*XunLeiCommon
model.Storage
ExpertAddition
identity string
}
func (x *ThunderExpert) Config() driver.Config {
return configExpert
}
func (x *ThunderExpert) GetAddition() driver.Additional {
return x.ExpertAddition
}
func (x *ThunderExpert) Init(ctx context.Context, storage model.Storage) (err error) {
x.Storage = storage
if err = utils.Json.UnmarshalFromString(x.Storage.Addition, &x.ExpertAddition); err != nil {
return err
}
// 防止重复登录
identity := x.GetIdentity()
if identity != x.identity || !x.IsLogin() {
x.identity = identity
x.XunLeiCommon = &XunLeiCommon{
Common: &Common{
client: base.NewRestyClient(),
DeviceID: x.DeviceID,
ClientID: x.ClientID,
ClientSecret: x.ClientSecret,
ClientVersion: x.ClientVersion,
PackageName: x.PackageName,
UserAgent: x.UserAgent,
DownloadUserAgent: x.DownloadUserAgent,
UseVideoUrl: x.UseVideoUrl,
},
}
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
// 签名方法
if x.SignType == "captcha_sign" {
x.Common.Timestamp = x.Timestamp
x.Common.CaptchaSign = x.CaptchaSign
} else {
x.Common.Algorithms = strings.Split(x.Algorithms, ",")
}
// 登录方式
if x.LoginType == "refresh_token" {
// 通过RefreshToken登录
token, err := x.XunLeiCommon.RefreshToken(x.ExpertAddition.RefreshToken)
if err != nil {
return err
}
x.SetTokenResp(token)
// 刷新token方法
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
} else {
// 通过用户密码登录
token, err := x.Login(x.Username, x.Password)
if err != nil {
return err
}
x.SetTokenResp(token)
x.SetRefreshTokenFunc(func() error {
token, err := x.XunLeiCommon.RefreshToken(x.TokenResp.RefreshToken)
if err != nil {
token, err = x.Login(x.Username, x.Password)
if err != nil {
x.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
}
}
x.SetTokenResp(token)
op.MustSaveDriverStorage(x)
return err
})
}
} else {
// 仅修改验证码token
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
x.XunLeiCommon.UserAgent = x.UserAgent
x.XunLeiCommon.DownloadUserAgent = x.DownloadUserAgent
x.XunLeiCommon.UseVideoUrl = x.UseVideoUrl
}
return nil
}
func (x *ThunderExpert) Drop(ctx context.Context) error {
return nil
}
func (x *ThunderExpert) SetTokenResp(token *TokenResp) {
x.XunLeiCommon.SetTokenResp(token)
if token != nil {
x.ExpertAddition.RefreshToken = token.RefreshToken
}
}
type XunLeiCommon struct {
*Common
*TokenResp // 登录信息
refreshTokenFunc func() error
}
func (xc *XunLeiCommon) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
return xc.getFiles(ctx, dir.GetID())
}
func (xc *XunLeiCommon) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
var lFile Files
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", file.GetID())
//r.SetQueryParam("space", "")
}, &lFile)
if err != nil {
return nil, err
}
link := &model.Link{
URL: lFile.WebContentLink,
Header: http.Header{
"User-Agent": {xc.DownloadUserAgent},
},
}
if xc.UseVideoUrl {
for _, media := range lFile.Medias {
if media.Link.URL != "" {
link.URL = media.Link.URL
break
}
}
}
/*
strs := regexp.MustCompile(`e=([0-9]*)`).FindStringSubmatch(lFile.WebContentLink)
if len(strs) == 2 {
timestamp, err := strconv.ParseInt(strs[1], 10, 64)
if err == nil {
expired := time.Duration(timestamp-time.Now().Unix()) * time.Second
link.Expiration = &expired
}
}
*/
return link, nil
}
func (xc *XunLeiCommon) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FOLDER,
"name": dirName,
"parent_id": parentDir.GetID(),
})
}, nil)
return err
}
func (xc *XunLeiCommon) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchMove", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiCommon) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", srcObj.GetID())
r.SetBody(&base.Json{"name": newName})
}, nil)
return err
}
func (xc *XunLeiCommon) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
_, err := xc.Request(FILE_API_URL+":batchCopy", http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"to": base.Json{"parent_id": dstDir.GetID()},
"ids": []string{srcObj.GetID()},
})
}, nil)
return err
}
func (xc *XunLeiCommon) Remove(ctx context.Context, obj model.Obj) error {
_, err := xc.Request(FILE_API_URL+"/{fileID}/trash", http.MethodPatch, func(r *resty.Request) {
r.SetContext(ctx)
r.SetPathParam("fileID", obj.GetID())
r.SetBody("{}")
}, nil)
return err
}
func (xc *XunLeiCommon) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
var resp UploadTaskResponse
_, err := xc.Request(FILE_API_URL, http.MethodPost, func(r *resty.Request) {
r.SetContext(ctx)
r.SetBody(&base.Json{
"kind": FILE,
"parent_id": dstDir.GetID(),
"name": stream.GetName(),
"size": stream.GetSize(),
"hash": "1CF254FBC456E1B012CD45C546636AA62CF8350E",
"upload_type": UPLOAD_TYPE_RESUMABLE,
})
}, &resp)
if err != nil {
return err
}
param := resp.Resumable.Params
if resp.UploadType == UPLOAD_TYPE_RESUMABLE {
param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".")
s, err := session.NewSession(&aws.Config{
Credentials: credentials.NewStaticCredentials(param.AccessKeyID, param.AccessKeySecret, param.SecurityToken),
Region: aws.String("xunlei"),
Endpoint: aws.String(param.Endpoint),
})
if err != nil {
return err
}
_, err = s3manager.NewUploader(s).UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(param.Bucket),
Key: aws.String(param.Key),
Expires: aws.Time(param.Expiration),
Body: stream,
})
return err
}
return nil
}
func (xc *XunLeiCommon) getFiles(ctx context.Context, folderId string) ([]model.Obj, error) {
files := make([]model.Obj, 0)
var pageToken string
for {
var fileList FileList
_, err := xc.Request(FILE_API_URL, http.MethodGet, func(r *resty.Request) {
r.SetContext(ctx)
r.SetQueryParams(map[string]string{
"space": "",
"__type": "drive",
"refresh": "true",
"__sync": "true",
"parent_id": folderId,
"page_token": pageToken,
"with_audit": "true",
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
})
}, &fileList)
if err != nil {
return nil, err
}
for i := 0; i < len(fileList.Files); i++ {
files = append(files, &fileList.Files[i])
}
if fileList.NextPageToken == "" {
break
}
pageToken = fileList.NextPageToken
}
return files, nil
}
// 设置刷新Token的方法
func (xc *XunLeiCommon) SetRefreshTokenFunc(fn func() error) {
xc.refreshTokenFunc = fn
}
// 设置Token
func (xc *XunLeiCommon) SetTokenResp(tr *TokenResp) {
xc.TokenResp = tr
}
// 携带Authorization和CaptchaToken的请求
func (xc *XunLeiCommon) Request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
data, err := xc.Common.Request(url, method, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Authorization": xc.Token(),
"X-Captcha-Token": xc.GetCaptchaToken(),
})
if callback != nil {
callback(req)
}
}, resp)
errResp, ok := err.(*ErrResp)
if !ok {
return nil, err
}
switch errResp.ErrorCode {
case 0:
return data, nil
case 4122, 4121, 10, 16:
if xc.refreshTokenFunc != nil {
if err = xc.refreshTokenFunc(); err == nil {
break
}
}
return nil, err
case 9: // 验证码token过期
if err = xc.RefreshCaptchaTokenAtLogin(GetAction(method, url), xc.UserID); err != nil {
return nil, err
}
default:
return nil, err
}
return xc.Request(url, method, callback, resp)
}
// 刷新Token
func (xc *XunLeiCommon) RefreshToken(refreshToken string) (*TokenResp, error) {
var resp TokenResp
_, err := xc.Common.Request(XLUSER_API_URL+"/auth/token", http.MethodPost, func(req *resty.Request) {
req.SetBody(&base.Json{
"grant_type": "refresh_token",
"refresh_token": refreshToken,
"client_id": xc.ClientID,
"client_secret": xc.ClientSecret,
})
}, &resp)
if err != nil {
return nil, err
}
if resp.RefreshToken == "" {
return nil, errs.EmptyToken
}
return &resp, nil
}
// 登录
func (xc *XunLeiCommon) Login(username, password string) (*TokenResp, error) {
url := XLUSER_API_URL + "/auth/signin"
err := xc.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), username)
if err != nil {
return nil, err
}
var resp TokenResp
_, err = xc.Common.Request(url, http.MethodPost, func(req *resty.Request) {
req.SetBody(&SignInRequest{
CaptchaToken: xc.GetCaptchaToken(),
ClientID: xc.ClientID,
ClientSecret: xc.ClientSecret,
Username: username,
Password: password,
})
}, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}
func (xc *XunLeiCommon) IsLogin() bool {
if xc.TokenResp == nil {
return false
}
_, err := xc.Request(XLUSER_API_URL+"/user/me", http.MethodGet, nil, nil)
return err == nil
}

102
drivers/thunder/meta.go Normal file
View File

@ -0,0 +1,102 @@
package thunder
import (
"crypto/md5"
"encoding/hex"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
)
// 高级设置
type ExpertAddition struct {
driver.RootID
LoginType string `json:"login_type" type:"select" options:"user,refresh_token" default:"user"`
SignType string `json:"sign_type" type:"select" options:"algorithms,captcha_sign" default:"algorithms"`
// 登录方式1
Username string `json:"username" required:"true" help:"login type is user,this is required"`
Password string `json:"password" required:"true" help:"login type is user,this is required"`
// 登录方式2
RefreshToken string `json:"refresh_token" required:"true" help:"login type is refresh_token,this is required"`
// 签名方法1
Algorithms string `json:"algorithms" required:"true" help:"sign type is algorithms,this is required" default:"HPxr4BVygTQVtQkIMwQH33ywbgYG5l4JoR,GzhNkZ8pOBsCY+7,v+l0ImTpG7c7/,e5ztohgVXNP,t,EbXUWyVVqQbQX39Mbjn2geok3/0WEkAVxeqhtx857++kjJiRheP8l77gO,o7dvYgbRMOpHXxCs,6MW8TD8DphmakaxCqVrfv7NReRRN7ck3KLnXBculD58MvxjFRqT+,kmo0HxCKVfmxoZswLB4bVA/dwqbVAYghSb,j,4scKJNdd7F27Hv7tbt"`
// 签名方法2
CaptchaSign string `json:"captcha_sign" required:"true" help:"sign type is captcha_sign,this is required"`
Timestamp string `json:"timestamp" required:"true" help:"sign type is captcha_sign,this is required"`
// 验证码
CaptchaToken string `json:"captcha_token"`
// 必要且影响登录,由签名决定
DeviceID string `json:"device_id" required:"true" default:"9aa5c268e7bcfc197a9ad88e2fb330e5"`
ClientID string `json:"client_id" required:"true" default:"Xp6vsxz_7IYVw2BB"`
ClientSecret string `json:"client_secret" required:"true" default:"Xp6vsy4tN9toTVdMSpomVdXpRmES"`
ClientVersion string `json:"client_version" required:"true" default:"7.51.0.8196"`
PackageName string `json:"package_name" required:"true" default:"com.xunlei.downloadprovider"`
//不影响登录,影响下载速度
UserAgent string `json:"user_agent" required:"true" default:"ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/4G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gdcf98eab238b) (JAVA 0)"`
DownloadUserAgent string `json:"download_user_agent" required:"true" default:"Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)"`
//优先使用视频链接代替下载链接
UseVideoUrl bool `json:"use_video_url"`
}
// 登录特征,用于判断是否重新登录
func (i *ExpertAddition) GetIdentity() string {
hash := md5.New()
if i.LoginType == "refresh_token" {
hash.Write([]byte(i.RefreshToken))
} else {
hash.Write([]byte(i.Username + i.Password))
}
if i.SignType == "captcha_sign" {
hash.Write([]byte(i.CaptchaSign + i.Timestamp))
} else {
hash.Write([]byte(i.Algorithms))
}
hash.Write([]byte(i.DeviceID))
hash.Write([]byte(i.ClientID))
hash.Write([]byte(i.ClientSecret))
hash.Write([]byte(i.ClientVersion))
hash.Write([]byte(i.PackageName))
return hex.EncodeToString(hash.Sum(nil))
}
type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
CaptchaToken string `json:"captcha_token"`
}
// 登录特征,用于判断是否重新登录
func (i *Addition) GetIdentity() string {
return utils.GetMD5Encode(i.Username + i.Password)
}
var config = driver.Config{
Name: "Thunder",
LocalSort: true,
OnlyProxy: true,
}
var configExpert = driver.Config{
Name: "ThunderExpert",
LocalSort: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &Thunder{}
})
op.RegisterDriver(configExpert, func() driver.Driver {
return &ThunderExpert{}
})
}

195
drivers/thunder/types.go Normal file
View File

@ -0,0 +1,195 @@
package thunder
import (
"fmt"
"strconv"
"time"
)
type ErrResp struct {
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
ErrorDescription string `json:"error_description"`
// ErrorDetails interface{} `json:"error_details"`
}
func (e *ErrResp) IsError() bool {
return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != ""
}
func (e *ErrResp) Error() string {
return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription)
}
/*
* 验证码Token
**/
type CaptchaTokenRequest struct {
Action string `json:"action"`
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
DeviceID string `json:"device_id"`
Meta map[string]string `json:"meta"`
RedirectUri string `json:"redirect_uri"`
}
type CaptchaTokenResponse struct {
CaptchaToken string `json:"captcha_token"`
ExpiresIn int64 `json:"expires_in"`
Url string `json:"url"`
}
/*
* 登录
**/
type TokenResp struct {
TokenType string `json:"token_type"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"`
Sub string `json:"sub"`
UserID string `json:"user_id"`
}
func (t *TokenResp) Token() string {
return fmt.Sprint(t.TokenType, " ", t.AccessToken)
}
type SignInRequest struct {
CaptchaToken string `json:"captcha_token"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
Username string `json:"username"`
Password string `json:"password"`
}
/*
* 文件
**/
type FileList struct {
Kind string `json:"kind"`
NextPageToken string `json:"next_page_token"`
Files []Files `json:"files"`
Version string `json:"version"`
VersionOutdated bool `json:"version_outdated"`
}
type Link struct {
URL string `json:"url"`
Token string `json:"token"`
Expire time.Time `json:"expire"`
Type string `json:"type"`
}
type Files struct {
Kind string `json:"kind"`
ID string `json:"id"`
ParentID string `json:"parent_id"`
Name string `json:"name"`
//UserID string `json:"user_id"`
Size string `json:"size"`
//Revision string `json:"revision"`
//FileExtension string `json:"file_extension"`
//MimeType string `json:"mime_type"`
//Starred bool `json:"starred"`
WebContentLink string `json:"web_content_link"`
CreatedTime time.Time `json:"created_time"`
ModifiedTime time.Time `json:"modified_time"`
IconLink string `json:"icon_link"`
ThumbnailLink string `json:"thumbnail_link"`
//Md5Checksum string `json:"md5_checksum"`
//Hash string `json:"hash"`
Links map[string]Link `json:"links"`
Phase string `json:"phase"`
Audit struct {
Status string `json:"status"`
Message string `json:"message"`
Title string `json:"title"`
} `json:"audit"`
Medias []struct {
Category string `json:"category"`
IconLink string `json:"icon_link"`
IsDefault bool `json:"is_default"`
IsOrigin bool `json:"is_origin"`
IsVisible bool `json:"is_visible"`
Link Link `json:"link"`
MediaID string `json:"media_id"`
MediaName string `json:"media_name"`
NeedMoreQuota bool `json:"need_more_quota"`
Priority int `json:"priority"`
RedirectLink string `json:"redirect_link"`
ResolutionName string `json:"resolution_name"`
Video struct {
AudioCodec string `json:"audio_codec"`
BitRate int `json:"bit_rate"`
Duration int `json:"duration"`
FrameRate int `json:"frame_rate"`
Height int `json:"height"`
VideoCodec string `json:"video_codec"`
VideoType string `json:"video_type"`
Width int `json:"width"`
} `json:"video"`
VipTypes []string `json:"vip_types"`
} `json:"medias"`
Trashed bool `json:"trashed"`
DeleteTime string `json:"delete_time"`
OriginalURL string `json:"original_url"`
//Params struct{} `json:"params"`
//OriginalFileIndex int `json:"original_file_index"`
//Space string `json:"space"`
//Apps []interface{} `json:"apps"`
//Writable bool `json:"writable"`
//FolderType string `json:"folder_type"`
//Collection interface{} `json:"collection"`
}
func (c *Files) GetSize() int64 { size, _ := strconv.ParseInt(c.Size, 10, 64); return size }
func (c *Files) GetName() string { return c.Name }
func (c *Files) ModTime() time.Time { return c.ModifiedTime }
func (c *Files) IsDir() bool { return c.Kind == FOLDER }
func (c *Files) GetID() string { return c.ID }
func (c *Files) GetPath() string { return "" }
func (c *Files) Thumb() string { return c.ThumbnailLink }
/*
* 上传
**/
type UploadTaskResponse struct {
UploadType string `json:"upload_type"`
/*//UPLOAD_TYPE_FORM
Form struct {
//Headers struct{} `json:"headers"`
Kind string `json:"kind"`
Method string `json:"method"`
MultiParts struct {
OSSAccessKeyID string `json:"OSSAccessKeyId"`
Signature string `json:"Signature"`
Callback string `json:"callback"`
Key string `json:"key"`
Policy string `json:"policy"`
XUserData string `json:"x:user_data"`
} `json:"multi_parts"`
URL string `json:"url"`
} `json:"form"`*/
//UPLOAD_TYPE_RESUMABLE
Resumable struct {
Kind string `json:"kind"`
Params struct {
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
Expiration time.Time `json:"expiration"`
Key string `json:"key"`
SecurityToken string `json:"security_token"`
} `json:"params"`
Provider string `json:"provider"`
} `json:"resumable"`
File Files `json:"file"`
}

167
drivers/thunder/util.go Normal file
View File

@ -0,0 +1,167 @@
package thunder
import (
"fmt"
"net/http"
"regexp"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
const (
API_URL = "https://api-pan.xunlei.com/drive/v1"
FILE_API_URL = API_URL + "/files"
XLUSER_API_URL = "https://xluser-ssl.xunlei.com/v1"
)
const (
FOLDER = "drive#folder"
FILE = "drive#file"
RESUMABLE = "drive#resumable"
)
const (
UPLOAD_TYPE_UNKNOWN = "UPLOAD_TYPE_UNKNOWN"
//UPLOAD_TYPE_FORM = "UPLOAD_TYPE_FORM"
UPLOAD_TYPE_RESUMABLE = "UPLOAD_TYPE_RESUMABLE"
UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL"
)
func GetAction(method string, url string) string {
urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1]
return method + ":" + urlpath
}
type Common struct {
client *resty.Client
captchaToken string
// 签名相关,二选一
Algorithms []string
Timestamp, CaptchaSign string
// 必要值,签名相关
DeviceID string
ClientID string
ClientSecret string
ClientVersion string
PackageName string
UserAgent string
DownloadUserAgent string
UseVideoUrl bool
}
func (c *Common) SetCaptchaToken(captchaToken string) {
c.captchaToken = captchaToken
}
func (c *Common) GetCaptchaToken() string {
return c.captchaToken
}
// 刷新验证码token(登录后)
func (c *Common) RefreshCaptchaTokenAtLogin(action, userID string) error {
metas := map[string]string{
"client_version": c.ClientVersion,
"package_name": c.PackageName,
"user_id": userID,
}
metas["timestamp"], metas["captcha_sign"] = c.GetCaptchaSign()
return c.refreshCaptchaToken(action, metas)
}
// 刷新验证码token(登录时)
func (c *Common) RefreshCaptchaTokenInLogin(action, username string) error {
metas := make(map[string]string)
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
metas["email"] = username
} else if len(username) >= 11 && len(username) <= 18 {
metas["phone_number"] = username
} else {
metas["username"] = username
}
return c.refreshCaptchaToken(action, metas)
}
// 获取验证码签名
func (c *Common) GetCaptchaSign() (timestamp, sign string) {
if len(c.Algorithms) == 0 {
return c.Timestamp, c.CaptchaSign
}
timestamp = fmt.Sprint(time.Now().UnixMilli())
str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp)
for _, algorithm := range c.Algorithms {
str = utils.GetMD5Encode(str + algorithm)
}
sign = "1." + str
return
}
// 刷新验证码token
func (c *Common) refreshCaptchaToken(action string, metas map[string]string) error {
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: c.captchaToken,
ClientID: c.ClientID,
DeviceID: c.DeviceID,
Meta: metas,
RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor",
}
var e ErrResp
var resp CaptchaTokenResponse
_, err := c.Request(XLUSER_API_URL+"/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
req.SetError(&e).SetBody(param)
}, &resp)
if err != nil {
return err
}
if e.IsError() {
return &e
}
if resp.Url != "" {
return fmt.Errorf("need verify:%s", resp.Url)
}
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
c.SetCaptchaToken(resp.CaptchaToken)
return nil
}
// 只有基础信息的请求
func (c *Common) Request(url, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := c.client.R().SetHeaders(map[string]string{
"user-agent": c.UserAgent,
"accept": "application/json;charset=UTF-8",
"x-device-id": c.DeviceID,
"x-client-id": c.ClientID,
"x-client-version": c.ClientVersion,
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
var erron ErrResp
utils.Json.Unmarshal(res.Body(), &erron)
if erron.IsError() {
return nil, &erron
}
return res.Body(), nil
}

View File

@ -9,7 +9,6 @@ import (
"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/upyun/go-sdk/v3/upyun"
@ -140,8 +139,4 @@ func (d *USS) Put(ctx context.Context, dstDir model.Obj, stream model.FileStream
})
}
func (d *USS) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*USS)(nil)

View File

@ -6,7 +6,6 @@ import (
"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"
@ -89,8 +88,4 @@ func (d *Virtual) Put(ctx context.Context, dstDir model.Obj, stream model.FileSt
return nil
}
func (d *Virtual) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*Virtual)(nil)

View File

@ -8,7 +8,6 @@ import (
"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/cron"
"github.com/alist-org/alist/v3/pkg/gowebdav"
@ -123,8 +122,4 @@ func (d *WebDav) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
return err
}
func (d *WebDav) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*WebDav)(nil)

View File

@ -8,7 +8,6 @@ import (
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
@ -139,8 +138,4 @@ func (d *YandexDisk) Put(ctx context.Context, dstDir model.Obj, stream model.Fil
return err
}
func (d *YandexDisk) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
return nil, errs.NotSupport
}
var _ driver.Driver = (*YandexDisk)(nil)

17
go.mod
View File

@ -1,23 +1,31 @@
module github.com/alist-org/alist/v3
go 1.18
go 1.19
require (
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
github.com/aws/aws-sdk-go v1.44.88
github.com/caarlos0/env/v6 v6.9.3
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.8.0
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4
github.com/json-iterator/go v1.1.12
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.5
github.com/pquerna/otp v1.3.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf
github.com/upyun/go-sdk/v3 v3.0.3
github.com/winfsp/cgofuse v1.5.0
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
gorm.io/driver/mysql v1.3.4
gorm.io/driver/postgres v1.3.7
gorm.io/driver/sqlite v1.3.4
@ -25,13 +33,11 @@ require (
)
require (
github.com/aws/aws-sdk-go v1.44.88 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -47,7 +53,6 @@ require (
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
@ -56,13 +61,9 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/upyun/go-sdk/v3 v3.0.3 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect

19
go.sum
View File

@ -125,6 +125,7 @@ github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4 h1:8bWaY08VCoFn17gezY
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4/go.mod h1:hhq4G4crv+nW2qXtNYcuzLeOudG92Ps37HEKeg2e3lE=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -211,9 +212,10 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf h1:Y43S3e9P1NPs/QF4R5/SdlXj2d31540hP4Gk8VKNvDg=
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf/go.mod h1:c+cGNU1qi9bO7ZF4IRMYk+KaZTNiQ/gQrSbyMmGFq1Q=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@ -235,6 +237,7 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -246,10 +249,8 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
@ -264,8 +265,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -285,14 +284,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -334,7 +330,6 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=

View File

@ -2,7 +2,6 @@ package aria2
import (
"fmt"
"mime"
"os"
"path"
"strconv"
@ -13,6 +12,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@ -76,10 +76,15 @@ func (m *Monitor) Update() (bool, error) {
}
m.retried = 0
if len(info.FollowedBy) != 0 {
log.Debugf("followen by: %+v", info.FollowedBy)
gid := info.FollowedBy[0]
notify.Signals.Delete(m.tsk.ID)
oldId := m.tsk.ID
m.tsk.ID = gid
DownTaskManager.RawTasks().Delete(oldId)
DownTaskManager.RawTasks().Store(m.tsk.ID, m.tsk)
notify.Signals.Store(gid, m.c)
return false, nil
}
// update download status
total, err := strconv.ParseUint(info.TotalLength, 10, 64)
@ -120,6 +125,7 @@ func (m *Monitor) Complete() error {
}
// get files
files, err := client.GetFiles(m.tsk.ID)
log.Debugf("files len: %d", len(files))
if err != nil {
return errors.Wrapf(err, "failed to get files of %s", m.tsk.ID)
}
@ -134,16 +140,14 @@ func (m *Monitor) Complete() error {
log.Errorf("failed to remove aria2 temp dir: %+v", err.Error())
}
}()
for _, file := range files {
for i, _ := range files {
file := files[i]
TransferTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("transfer %s to [%s](%s)", file.Path, storage.GetStorage().MountPath, dstDirActualPath),
Func: func(tsk *task.Task[uint64]) error {
defer wg.Done()
size, _ := strconv.ParseInt(file.Length, 10, 64)
mimetype := mime.TypeByExtension(path.Ext(file.Path))
if mimetype == "" {
mimetype = "application/octet-stream"
}
mimetype := utils.GetMimeType(file.Path)
f, err := os.Open(file.Path)
if err != nil {
return errors.Wrapf(err, "failed to open file %s", file.Path)

View File

@ -6,8 +6,9 @@ import (
)
func InitAria2() {
go func() {
_, err := aria2.InitClient(2)
utils.Log.Errorf("failed to init aria2 client: %+v", err)
}()
_, err := aria2.InitClient(2)
if err != nil {
//utils.Log.Errorf("failed to init aria2 client: %+v", err)
utils.Log.Infof("Aria2 not ready.")
}
}

View File

@ -1,7 +1,6 @@
package bootstrap
import (
"io/ioutil"
"os"
"path/filepath"
@ -25,7 +24,7 @@ func InitConfig() {
log.Fatalf("failed to create default config file")
}
} else {
configBytes, err := ioutil.ReadFile(flags.Config)
configBytes, err := os.ReadFile(flags.Config)
if err != nil {
log.Fatalf("reading config file error: %+v", err)
}
@ -39,7 +38,7 @@ func InitConfig() {
if err != nil {
log.Fatalf("marshal config error: %+v", err)
}
err = ioutil.WriteFile(flags.Config, confBody, 0777)
err = os.WriteFile(flags.Config, confBody, 0777)
if err != nil {
log.Fatalf("update config struct error: %+v", err)
}
@ -59,7 +58,7 @@ func InitConfig() {
if err != nil {
log.Errorln("failed delete temp file:", err)
}
err = os.MkdirAll(conf.Conf.TempDir, 0700)
err = os.MkdirAll(conf.Conf.TempDir, 0777)
if err != nil {
log.Fatalf("create temp dir error: %+v", err)
}

View File

@ -34,7 +34,7 @@ func initSettings() {
// insert new items
for i := range initialSettingItems {
v := initialSettingItems[i]
_, err := db.GetSettingItemByKey(v.Key)
stored, err := db.GetSettingItemByKey(v.Key)
if errors.Is(err, gorm.ErrRecordNotFound) || v.Key == conf.VERSION {
err = db.SaveSettingItem(v)
if err != nil {
@ -42,6 +42,12 @@ func initSettings() {
}
} else if err != nil {
log.Fatalf("failed get setting: %+v", err)
} else {
v.Value = stored.Value
err = db.SaveSettingItem(v)
if err != nil {
log.Fatalf("failed resave setting: %+v", err)
}
}
}
}
@ -65,14 +71,14 @@ func InitialSettings() []model.SettingItem {
initialSettingItems = []model.SettingItem{
// site settings
{Key: conf.VERSION, Value: conf.Version, Type: conf.TypeString, Group: model.SITE, Flag: model.READONLY},
{Key: conf.ApiUrl, Value: "", Type: conf.TypeString, Group: model.SITE},
{Key: conf.BasePath, Value: "", Type: conf.TypeString, Group: model.SITE},
//{Key: conf.ApiUrl, Value: "", Type: conf.TypeString, Group: model.SITE},
//{Key: conf.BasePath, Value: "", Type: conf.TypeString, Group: model.SITE},
{Key: conf.SiteTitle, Value: "AList", Type: conf.TypeString, Group: model.SITE},
{Key: conf.Announcement, Value: "### repo\nhttps://github.com/alist-org/alist", Type: conf.TypeText, Group: model.SITE},
{Key: "pagination_type", Value: "all", Type: conf.TypeSelect, Options: "all,pagination,load_more,auto_load_more", Group: model.SITE},
{Key: "default_page_size", Value: "30", Type: conf.TypeNumber, Group: model.SITE},
// style settings
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
{Key: conf.Logo, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeText, Group: model.STYLE},
{Key: conf.Favicon, Value: "https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg", Type: conf.TypeString, Group: model.STYLE},
{Key: conf.MainColor, Value: "#1890ff", Type: conf.TypeString, Group: model.STYLE},
{Key: "home_icon", Value: "🏠", Type: conf.TypeString, Group: model.STYLE},
@ -91,7 +97,7 @@ func InitialSettings() []model.SettingItem {
"Google":"https://docs.google.com/gview?url=$e_url&embedded=true"
},
"pdf": {
"PDF.js":"https://alist.nn.ci/pdf.js/web/viewer.html?file=$e_url"
"PDF.js":"https://alist-org.github.io/pdf.js/web/viewer.html?file=$e_url"
}
}`, Type: conf.TypeText, Group: model.PREVIEW},
// {Key: conf.OfficeViewers, Value: `{
@ -107,7 +113,7 @@ func InitialSettings() []model.SettingItem {
// global settings
{Key: conf.HideFiles, Value: "/\\/README.md/i", Type: conf.TypeText, Group: model.GLOBAL},
{Key: "package_download", Value: "true", Type: conf.TypeBool, Group: model.GLOBAL},
{Key: conf.CustomizeHead, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.CustomizeHead, Value: `<script src="https://polyfill.io/v3/polyfill.min.js?features=String.prototype.replaceAll"></script>`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.CustomizeBody, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.LinkExpiration, Value: "0", Type: conf.TypeNumber, Group: model.GLOBAL, Flag: model.PRIVATE},
{Key: conf.PrivacyRegs, Value: `(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])

View File

@ -23,7 +23,7 @@ type Scheme struct {
}
type LogConfig struct {
Enable bool `json:"enable" env:"log_enable"`
Enable bool `json:"enable" env:"LOG_ENABLE"`
Name string `json:"name" env:"LOG_NAME"`
MaxSize int `json:"max_size" env:"MAX_SIZE"`
MaxBackups int `json:"max_backups" env:"MAX_BACKUPS"`
@ -32,32 +32,32 @@ type LogConfig struct {
}
type Config struct {
Force bool `json:"force"`
Address string `json:"address" env:"ADDR"`
Port int `json:"port" env:"PORT"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
// CaCheExpiration int `json:"cache_expiration" env:"CACHE_EXPIRATION"`
Cdn string `json:"cdn" env:"CDN"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
Log LogConfig `json:"log"`
Force bool `json:"force" env:"FORCE"`
Address string `json:"address" env:"ADDR"`
Port int `json:"port" env:"PORT"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
Log LogConfig `json:"log"`
}
func DefaultConfig() *Config {
return &Config{
Address: "0.0.0.0",
Port: 5244,
JwtSecret: random.String(16),
Cdn: "",
TempDir: "data/temp",
Address: "0.0.0.0",
Port: 5244,
JwtSecret: random.String(16),
TokenExpiresIn: 48,
TempDir: "data/temp",
Database: Database{
Type: "sqlite3",
Port: 0,
TablePrefix: "x_",
DBFile: "data/data.db",
},
// CaCheExpiration: 30,
Log: LogConfig{
Enable: true,
Name: "log/log.log",

View File

@ -3,15 +3,21 @@ package db
import (
"log"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"gorm.io/gorm"
)
var db gorm.DB
var db *gorm.DB
func Init(d *gorm.DB) {
db = *d
err := db.AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem))
db = d
var err error
if conf.Conf.Database.Type == "mysql" {
err = db.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4").AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem))
} else {
err = db.AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem))
}
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}

View File

@ -10,7 +10,7 @@ type Driver interface {
Meta
Reader
Writer
Other
//Other
}
type Meta interface {

View File

@ -28,11 +28,11 @@ type IRootId interface {
}
type RootPath struct {
RootFolderPath string `json:"root_folder_path" required:"true"`
RootFolderPath string `json:"root_folder_path"`
}
type RootID struct {
RootFolderID string `json:"root_folder_id" required:"true"`
RootFolderID string `json:"root_folder_id"`
}
func (r RootPath) GetRootPath() string {

View File

@ -12,6 +12,7 @@ import (
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var CopyTaskManager = task.NewTaskManager(3, func(tid *uint64) {
@ -60,7 +61,7 @@ func copyBetween2Storages(t *task.Task[uint64], srcStorage, dstStorage driver.Dr
return nil
}
srcObjPath := stdpath.Join(srcObjPath, obj.GetName())
dstObjPath := stdpath.Join(dstDirPath, obj.GetName())
dstObjPath := stdpath.Join(dstDirPath, srcObj.GetName())
CopyTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("copy [%s](%s) to [%s](%s)", srcStorage.GetStorage().MountPath, srcObjPath, dstStorage.GetStorage().MountPath, dstObjPath),
Func: func(t *task.Task[uint64]) error {
@ -72,7 +73,9 @@ func copyBetween2Storages(t *task.Task[uint64], srcStorage, dstStorage driver.Dr
CopyTaskManager.Submit(task.WithCancelCtx(&task.Task[uint64]{
Name: fmt.Sprintf("copy [%s](%s) to [%s](%s)", srcStorage.GetStorage().MountPath, srcObjPath, dstStorage.GetStorage().MountPath, dstDirPath),
Func: func(t *task.Task[uint64]) error {
return copyFileBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstDirPath)
err := copyFileBetween2Storages(t, srcStorage, dstStorage, srcObjPath, dstDirPath)
log.Debugf("copy file between storages: %+v", err)
return err
},
}))
}

View File

@ -16,37 +16,43 @@ import (
func list(ctx context.Context, path string, refresh ...bool) ([]model.Obj, error) {
meta := ctx.Value("meta").(*model.Meta)
user := ctx.Value("user").(*model.User)
var objs []model.Obj
storage, actualPath, err := op.GetStorageAndActualPath(path)
virtualFiles := op.GetStorageVirtualFilesByPath(path)
if err != nil {
if len(virtualFiles) != 0 {
return virtualFiles, nil
if len(virtualFiles) == 0 {
return nil, errors.WithMessage(err, "failed get storage")
}
return nil, errors.WithMessage(err, "failed get storage")
}
objs, err := op.List(ctx, storage, actualPath, model.ListArgs{
ReqPath: path,
}, refresh...)
if err != nil {
log.Errorf("%+v", err)
if len(virtualFiles) != 0 {
return virtualFiles, nil
} else {
objs, err = op.List(ctx, storage, actualPath, model.ListArgs{
ReqPath: path,
}, refresh...)
if err != nil {
log.Errorf("%+v", err)
if len(virtualFiles) == 0 {
return nil, errors.WithMessage(err, "failed get objs")
}
}
return nil, errors.WithMessage(err, "failed get objs")
}
for _, storageFile := range virtualFiles {
if !containsByName(objs, storageFile) {
objs = append(objs, storageFile)
if objs == nil {
objs = virtualFiles
} else {
for _, storageFile := range virtualFiles {
if !containsByName(objs, storageFile) {
objs = append(objs, storageFile)
}
}
}
if whetherHide(user, meta, path) {
objs = hide(objs, meta)
}
// sort objs
if storage.Config().LocalSort {
model.SortFiles(objs, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)
if storage != nil {
if storage.Config().LocalSort {
model.SortFiles(objs, storage.GetStorage().OrderBy, storage.GetStorage().OrderDirection)
}
model.ExtractFolder(objs, storage.GetStorage().ExtractFolder)
}
model.ExtractFolder(objs, storage.GetStorage().ExtractFolder)
return objs, nil
}

View File

@ -1,15 +1,18 @@
package fs
import (
"fmt"
"io"
"mime"
"net/http"
"os"
stdpath "path"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/google/uuid"
"github.com/pkg/errors"
)
@ -34,11 +37,17 @@ var httpClient = &http.Client{}
func getFileStreamFromLink(file model.Obj, link *model.Link) (model.FileStreamer, error) {
var rc io.ReadCloser
mimetype := mime.TypeByExtension(stdpath.Ext(file.GetName()))
mimetype := utils.GetMimeType(file.GetName())
if link.Data != nil {
rc = link.Data
} else if link.FilePath != nil {
f, err := os.Open(*link.FilePath)
// copy a new temp, because will be deleted after upload
newFilePath := stdpath.Join(conf.Conf.TempDir, fmt.Sprintf("%s-%s", uuid.NewString(), file.GetName()))
err := utils.CopyFile(*link.FilePath, newFilePath)
if err != nil {
return nil, err
}
f, err := os.Open(newFilePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to open file %s", *link.FilePath)
}

View File

@ -58,7 +58,7 @@ func SortFiles(objs []Obj, orderBy, orderDirection string) {
}
return objs[i].GetSize() <= objs[j].GetSize()
}
case "updated_at":
case "modified":
if orderDirection == "desc" {
return objs[i].ModTime().After(objs[j].ModTime())
}

View File

@ -145,8 +145,11 @@ func getAdditionalItems(t reflect.Type, defaultRoot string) []driver.Item {
if tag.Get("type") != "" {
item.Type = tag.Get("type")
}
if (item.Name == "root_folder_id" || item.Name == "root_folder_path") && item.Default == "" {
item.Default = defaultRoot
if item.Name == "root_folder_id" || item.Name == "root_folder_path" {
if item.Default == "" {
item.Default = defaultRoot
}
item.Required = item.Default != ""
}
// set default type to string
if item.Type == "" {

View File

@ -27,6 +27,10 @@ func ClearCache(storage driver.Driver, path string) {
listCache.Del(key)
}
func Key(storage driver.Driver, path string) string {
return stdpath.Join(storage.GetStorage().MountPath, utils.StandardizePath(path))
}
// List files in storage, not contains virtual file
func List(ctx context.Context, storage driver.Driver, path string, args model.ListArgs, refresh ...bool) ([]model.Obj, error) {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
@ -34,6 +38,13 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
}
path = utils.StandardizePath(path)
log.Debugf("op.List %s", path)
key := Key(storage, path)
if len(refresh) == 0 || !refresh[0] {
if files, ok := listCache.Get(key); ok {
log.Debugf("use cache when list %s", path)
return files, nil
}
}
dir, err := Get(ctx, storage, path)
if err != nil {
return nil, errors.WithMessage(err, "failed get dir")
@ -42,22 +53,14 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
if !dir.IsDir() {
return nil, errors.WithStack(errs.NotFolder)
}
if storage.Config().NoCache {
objs, err := storage.List(ctx, dir, args)
return objs, errors.WithStack(err)
}
key := stdpath.Join(storage.GetStorage().MountPath, path)
if len(refresh) == 0 || !refresh[0] {
if files, ok := listCache.Get(key); ok {
return files, nil
}
}
objs, err, _ := listG.Do(key, func() ([]model.Obj, error) {
files, err := storage.List(ctx, dir, args)
if err != nil {
return nil, errors.Wrapf(err, "failed to list objs")
}
listCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
if !storage.Config().NoCache && len(files) > 0 {
listCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
}
return files, nil
})
return objs, err
@ -124,6 +127,7 @@ func Get(ctx context.Context, storage driver.Driver, path string) (model.Obj, er
return f, nil
}
}
log.Debugf("cant find obj with name: %s", name)
return nil, errors.WithStack(errs.ObjectNotFound)
}
@ -166,44 +170,60 @@ func Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (
if err != nil {
return nil, errors.WithMessagef(err, "failed to get obj")
}
return storage.Other(ctx, model.OtherArgs{
Obj: obj,
Method: args.Method,
Data: args.Data,
})
if o, ok := storage.(driver.Other); ok {
return o.Other(ctx, model.OtherArgs{
Obj: obj,
Method: args.Method,
Data: args.Data,
})
} else {
return nil, errs.NotImplement
}
}
var mkdirG singleflight.Group[interface{}]
func MakeDir(ctx context.Context, storage driver.Driver, path string) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
}
// check if dir exists
f, err := Get(ctx, storage, path)
if err != nil {
if errs.IsObjectNotFound(err) {
parentPath, dirName := stdpath.Split(path)
err = MakeDir(ctx, storage, parentPath)
if err != nil {
return errors.WithMessagef(err, "failed to make parent dir [%s]", parentPath)
path = utils.StandardizePath(path)
key := Key(storage, path)
_, err, _ := mkdirG.Do(key, func() (interface{}, error) {
// check if dir exists
f, err := Get(ctx, storage, path)
if err != nil {
if errs.IsObjectNotFound(err) {
parentPath, dirName := stdpath.Split(path)
err = MakeDir(ctx, storage, parentPath)
if err != nil {
return nil, errors.WithMessagef(err, "failed to make parent dir [%s]", parentPath)
}
parentDir, err := Get(ctx, storage, parentPath)
// this should not happen
if err != nil {
return nil, errors.WithMessagef(err, "failed to get parent dir [%s]", parentPath)
}
err = storage.MakeDir(ctx, parentDir, dirName)
if err == nil {
ClearCache(storage, parentPath)
}
return nil, errors.WithStack(err)
} else {
return nil, errors.WithMessage(err, "failed to check if dir exists")
}
parentDir, err := Get(ctx, storage, parentPath)
// this should not happen
if err != nil {
return errors.WithMessagef(err, "failed to get parent dir [%s]", parentPath)
} else {
// dir exists
if f.IsDir() {
return nil, nil
} else {
// dir to make is a file
return nil, errors.New("file exists")
}
return errors.WithStack(storage.MakeDir(ctx, parentDir, dirName))
} else {
return errors.WithMessage(err, "failed to check if dir exists")
}
} else {
// dir exists
if f.IsDir() {
return nil
} else {
// dir to make is a file
return errors.New("file exists")
}
}
})
return err
}
func Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string) error {
@ -257,7 +277,28 @@ func Remove(ctx context.Context, storage driver.Driver, path string) error {
}
return errors.WithMessage(err, "failed to get object")
}
return errors.WithStack(storage.Remove(ctx, obj))
err = storage.Remove(ctx, obj)
if err == nil {
key := Key(storage, stdpath.Dir(path))
if objs, ok := listCache.Get(key); ok {
j := -1
for i, m := range objs {
if m.GetName() == obj.GetName() {
j = i
break
}
}
if j >= 0 && j < len(objs) {
objs = append(objs[:j], objs[j+1:]...)
listCache.Set(key, objs)
} else {
log.Debugf("not found obj")
}
} else {
log.Debugf("not found parent cache")
}
}
return errors.WithStack(err)
}
func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file model.FileStreamer, up driver.UpdateProgress) error {
@ -304,12 +345,10 @@ func Put(ctx context.Context, storage driver.Driver, dstDirPath string, file mod
}
err = storage.Put(ctx, parentDir, file, up)
log.Debugf("put file [%s] done", file.GetName())
if err == nil {
// set as complete
up(100)
// clear cache
key := stdpath.Join(storage.GetStorage().MountPath, dstDirPath)
listCache.Del(key)
}
//if err == nil {
// //clear cache
// key := stdpath.Join(storage.GetStorage().MountPath, dstDirPath)
// listCache.Del(key)
//}
return errors.WithStack(err)
}

View File

@ -5,7 +5,6 @@ import (
"strings"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -24,9 +23,10 @@ func ActualPath(storage driver.Additional, rawPath string) string {
// for path: remove the mount path prefix and join the actual root folder if exists
func GetStorageAndActualPath(rawPath string) (driver.Driver, string, error) {
rawPath = utils.StandardizePath(rawPath)
if strings.Contains(rawPath, "..") {
return nil, "", errors.WithStack(errs.RelativePath)
}
// why can remove this check? because reqPath has joined the base_path of user, no relative path
//if strings.Contains(rawPath, "..") {
// return nil, "", errors.WithStack(errs.RelativePath)
//}
storage := GetBalancedStorage(rawPath)
if storage == nil {
return nil, "", errors.Errorf("can't find storage with rawPath: %s", rawPath)

View File

@ -94,19 +94,14 @@ func EnableStorage(ctx context.Context, id uint) error {
if !storage.Disabled {
return errors.Errorf("this storage have enabled")
}
err = LoadStorage(ctx, *storage)
if err != nil {
return errors.WithMessage(err, "failed load storage")
}
// re-get storage from db, because it maybe hava updated
storage, err = db.GetStorageById(id)
if err != nil {
return errors.WithMessage(err, "failed re-get storage again")
}
storage.Disabled = false
err = db.UpdateStorage(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in db, but have load in memory. you can try restart")
return errors.WithMessage(err, "failed update storage in db")
}
err = LoadStorage(ctx, *storage)
if err != nil {
return errors.WithMessage(err, "failed load storage")
}
return nil
}
@ -128,12 +123,12 @@ func DisableStorage(ctx context.Context, id uint) error {
return errors.Wrapf(err, "failed drop storage")
}
// delete the storage in the memory
storagesMap.Delete(storage.MountPath)
storage.Disabled = true
err = db.UpdateStorage(storage)
if err != nil {
return errors.WithMessage(err, "failed update storage in db, but have drop in memory. you can try restart")
return errors.WithMessage(err, "failed update storage in db")
}
storagesMap.Delete(storage.MountPath)
return nil
}
@ -187,13 +182,15 @@ func DeleteStorageById(ctx context.Context, id uint) error {
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
storageDriver, err := GetStorageByVirtualPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrapf(err, "failed drop storage")
if !storage.Disabled {
storageDriver, err := GetStorageByVirtualPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrapf(err, "failed drop storage")
}
}
// delete the storage in the database
if err := db.DeleteStorageById(id); err != nil {

62
pkg/chanio/chanio.go Normal file
View File

@ -0,0 +1,62 @@
package chanio
import (
"io"
"sync/atomic"
)
type ChanIO struct {
cl atomic.Bool
c chan []byte
buf []byte
}
func New() *ChanIO {
return &ChanIO{
cl: atomic.Bool{},
c: make(chan []byte),
buf: make([]byte, 0),
}
}
func (c *ChanIO) Read(p []byte) (int, error) {
if c.cl.Load() {
if len(c.buf) == 0 {
return 0, io.EOF
}
n := copy(p, c.buf)
if len(c.buf) > n {
c.buf = c.buf[n:]
} else {
c.buf = make([]byte, 0)
}
return n, nil
}
for len(c.buf) < len(p) && !c.cl.Load() {
c.buf = append(c.buf, <-c.c...)
}
n := copy(p, c.buf)
if len(c.buf) > n {
c.buf = c.buf[n:]
} else {
c.buf = make([]byte, 0)
}
return n, nil
}
func (c *ChanIO) Write(p []byte) (int, error) {
if c.cl.Load() {
return 0, io.ErrClosedPipe
}
c.c <- p
return len(p), nil
}
func (c *ChanIO) Close() error {
if c.cl.Load() {
return io.ErrClosedPipe
}
c.cl.Store(true)
close(c.c)
return nil
}

View File

@ -30,6 +30,10 @@ func (c *Cron) Do(f func()) {
}
func (c *Cron) Stop() {
c.ch <- struct{}{}
close(c.ch)
select {
case _, _ = <-c.ch:
default:
c.ch <- struct{}{}
close(c.ch)
}
}

View File

@ -10,6 +10,7 @@ func TestCron(t *testing.T) {
c.Do(func() {
t.Logf("cron log")
})
time.Sleep(time.Second * 5)
time.Sleep(time.Second * 3)
c.Stop()
c.Stop()
}

View File

@ -122,6 +122,10 @@ func (tm *Manager[K]) ClearDone() {
tm.RemoveByStates(SUCCEEDED, CANCELED, ERRORED)
}
func (tm *Manager[K]) RawTasks() *generic_sync.MapOf[K, *Task[K]] {
return &tm.tasks
}
func NewTaskManager[K comparable](maxWorker int, updateID ...func(*K)) *Manager[K] {
tm := &Manager[K]{
tasks: generic_sync.MapOf[K, *Task[K]]{},

View File

@ -82,6 +82,7 @@ func (t *Task[K]) run() {
t.state = ERRORED
} else {
t.state = SUCCEEDED
t.SetProgress(100)
if t.callback != nil {
t.callback(t)
}

View File

@ -1,14 +1,77 @@
package utils
import (
"fmt"
"io"
"io/ioutil"
"mime"
"os"
"path"
"path/filepath"
"strings"
"github.com/alist-org/alist/v3/internal/conf"
log "github.com/sirupsen/logrus"
)
// CopyFile File copies a single file from src to dst
func CopyFile(src, dst string) error {
var err error
var srcfd *os.File
var dstfd *os.File
var srcinfo os.FileInfo
if srcfd, err = os.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = CreateNestedFile(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = os.Stat(src); err != nil {
return err
}
return os.Chmod(dst, srcinfo.Mode())
}
// CopyDir Dir copies a whole directory recursively
func CopyDir(src string, dst string) error {
var err error
var fds []os.FileInfo
var srcinfo os.FileInfo
if srcinfo, err = os.Stat(src); err != nil {
return err
}
if err = os.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = ioutil.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := path.Join(src, fd.Name())
dstfp := path.Join(dst, fd.Name())
if fd.IsDir() {
if err = CopyDir(srcfp, dstfp); err != nil {
fmt.Println(err)
}
} else {
if err = CopyFile(srcfp, dstfp); err != nil {
fmt.Println(err)
}
}
}
return nil
}
// Exists determine whether the file exists
func Exists(name string) bool {
if _, err := os.Stat(name); err != nil {
@ -56,7 +119,7 @@ func CreateTempFile(r io.ReadCloser) (*os.File, error) {
// GetFileType get file type
func GetFileType(filename string) int {
ext := Ext(filename)
ext := strings.ToLower(Ext(filename))
//if SliceContains(conf.TypesMap[conf.OfficeTypes], ext) {
// return conf.OFFICE
//}
@ -74,3 +137,12 @@ func GetFileType(filename string) int {
}
return conf.UNKNOWN
}
func GetMimeType(name string) string {
ext := path.Ext(name)
m := mime.TypeByExtension(ext)
if m != "" {
return m
}
return "application/octet-stream"
}

View File

@ -3,7 +3,9 @@ package utils
import (
"crypto/md5"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"strings"
)
func GetSHA1Encode(data string) string {
@ -17,3 +19,20 @@ func GetMD5Encode(data string) string {
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
var DEC = map[string]string{
"-": "+",
"_": "/",
".": "=",
}
func SafeAtob(data string) (string, error) {
for k, v := range DEC {
data = strings.ReplaceAll(data, k, v)
}
bytes, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return "", err
}
return string(bytes), err
}

View File

@ -40,3 +40,32 @@ func CopyWithCtx(ctx context.Context, out io.Writer, in io.Reader, size int64, p
}))
return err
}
type limitWriter struct {
w io.Writer
count int64
limit int64
}
func (l limitWriter) Write(p []byte) (n int, err error) {
wn := int(l.limit - l.count)
if wn > len(p) {
wn = len(p)
}
if wn > 0 {
if n, err = l.w.Write(p[:wn]); err != nil {
return
}
if n < wn {
err = io.ErrShortWrite
}
}
if err == nil {
n = len(p)
}
return
}
func LimitWriter(w io.Writer, size int64) io.Writer {
return &limitWriter{w: w, limit: size}
}

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