dan92 commited on
Commit
2db4dde
·
verified ·
1 Parent(s): ffcbaf0

Upload 590 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.air.toml ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root = "."
2
+ testdata_dir = "testdata"
3
+ tmp_dir = "tmp"
4
+
5
+ [build]
6
+ args_bin = ["server"]
7
+ bin = "./tmp/main"
8
+ cmd = "go build -o ./tmp/main ."
9
+ delay = 0
10
+ exclude_dir = ["assets", "tmp", "vendor", "testdata"]
11
+ exclude_file = []
12
+ exclude_regex = ["_test.go"]
13
+ exclude_unchanged = false
14
+ follow_symlink = false
15
+ full_bin = ""
16
+ include_dir = []
17
+ include_ext = ["go", "tpl", "tmpl", "html"]
18
+ include_file = []
19
+ kill_delay = "0s"
20
+ log = "build-errors.log"
21
+ poll = false
22
+ poll_interval = 0
23
+ rerun = false
24
+ rerun_delay = 500
25
+ send_interrupt = false
26
+ stop_on_error = false
27
+
28
+ [color]
29
+ app = ""
30
+ build = "yellow"
31
+ main = "magenta"
32
+ runner = "green"
33
+ watcher = "cyan"
34
+
35
+ [log]
36
+ main_only = false
37
+ time = false
38
+
39
+ [misc]
40
+ clean_on_exit = false
41
+
42
+ [screen]
43
+ clear_on_rebuild = false
44
+ keep_scroll = true
.gitattributes CHANGED
@@ -1,35 +1 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ .github
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .idea/
2
+ .DS_Store
3
+ output/
4
+ /dist/
5
+
6
+ # Binaries for programs and plugins
7
+ *.exe
8
+ *.exe~
9
+ *.dll
10
+ *.so
11
+ *.dylib
12
+ *.db
13
+ *.bin
14
+
15
+ # Test binary, built with `go test -c`
16
+ *.test
17
+
18
+ # Output of the go coverage tool, specifically when used with LiteIDE
19
+ *.out
20
+
21
+ # Dependency directories (remove the comment below to include it)
22
+ # vendor/
23
+ /bin/*
24
+ *.json
25
+ /build
26
+ /data/
27
+ /tmp/
28
+ /log/
29
+ /lang/
30
+ /daemon/
31
+ /public/dist/*
32
+ !/public/dist/README.md
33
+
34
+ .VSCodeCounter
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ i@nn.ci.
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
CONTRIBUTING.md ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing
2
+
3
+ ## Setup your machine
4
+
5
+ `alist` is written in [Go](https://golang.org/) and [React](https://reactjs.org/).
6
+
7
+ Prerequisites:
8
+
9
+ - [git](https://git-scm.com)
10
+ - [Go 1.20+](https://golang.org/doc/install)
11
+ - [gcc](https://gcc.gnu.org/)
12
+ - [nodejs](https://nodejs.org/)
13
+
14
+ Clone `alist` and `alist-web` anywhere:
15
+
16
+ ```shell
17
+ $ git clone https://github.com/alist-org/alist.git
18
+ $ git clone --recurse-submodules https://github.com/alist-org/alist-web.git
19
+ ```
20
+ You should switch to the `main` branch for development.
21
+
22
+ ## Preview your change
23
+ ### backend
24
+ ```shell
25
+ $ go run main.go
26
+ ```
27
+ ### frontend
28
+ ```shell
29
+ $ pnpm dev
30
+ ```
31
+
32
+ ## Add a new driver
33
+ Copy `drivers/template` folder and rename it, and follow the comments in it.
34
+
35
+ ## Create a commit
36
+
37
+ Commit messages should be well formatted, and to make that "standardized".
38
+
39
+ ### Commit Message Format
40
+ Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
41
+ format that includes a **type**, a **scope** and a **subject**:
42
+
43
+ ```
44
+ <type>(<scope>): <subject>
45
+ <BLANK LINE>
46
+ <body>
47
+ <BLANK LINE>
48
+ <footer>
49
+ ```
50
+
51
+ The **header** is mandatory and the **scope** of the header is optional.
52
+
53
+ Any line of the commit message cannot be longer than 100 characters! This allows the message to be easier
54
+ to read on GitHub as well as in various git tools.
55
+
56
+ ### Revert
57
+ If the commit reverts a previous commit, it should begin with `revert: `, followed by the header
58
+ of the reverted commit.
59
+ In the body it should say: `This reverts commit <hash>.`, where the hash is the SHA of the commit
60
+ being reverted.
61
+
62
+ ### Type
63
+ Must be one of the following:
64
+
65
+ * **feat**: A new feature
66
+ * **fix**: A bug fix
67
+ * **docs**: Documentation only changes
68
+ * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
69
+ semi-colons, etc)
70
+ * **refactor**: A code change that neither fixes a bug nor adds a feature
71
+ * **perf**: A code change that improves performance
72
+ * **test**: Adding missing or correcting existing tests
73
+ * **build**: Affects project builds or dependency modifications
74
+ * **revert**: Restore the previous commit
75
+ * **ci**: Continuous integration of related file modifications
76
+ * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
77
+ generation
78
+ * **release**: Release a new version
79
+
80
+ ### Scope
81
+ The scope could be anything specifying place of the commit change. For example `$location`,
82
+ `$browser`, `$compile`, `$rootScope`, `ngHref`, `ngClick`, `ngView`, etc...
83
+
84
+ You can use `*` when the change affects more than a single scope.
85
+
86
+ ### Subject
87
+ The subject contains succinct description of the change:
88
+
89
+ * use the imperative, present tense: "change" not "changed" nor "changes"
90
+ * don't capitalize first letter
91
+ * no dot (.) at the end
92
+
93
+ ### Body
94
+ Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
95
+ The body should include the motivation for the change and contrast this with previous behavior.
96
+
97
+ ### Footer
98
+ The footer should contain any information about **Breaking Changes** and is also the place to
99
+ [reference GitHub issues that this commit closes](https://help.github.com/articles/closing-issues-via-commit-messages/).
100
+
101
+ **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines.
102
+ The rest of the commit message is then used for this.
103
+
104
+ ## Submit a pull request
105
+
106
+ Push your branch to your `alist` fork and open a pull request against the
107
+ `main` branch.
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM alpine:latest
2
+
3
+ RUN apk add --no-cache python3 py3-pip curl tar gzip bash jq
4
+
5
+ RUN adduser -D -u 1000 user
6
+
7
+ RUN mkdir -p /home/user/data && chown -R user:user /home/user/data
8
+
9
+ ENV HOME=/home/user \
10
+ PATH=/home/user/.local/bin:$PATH
11
+
12
+ WORKDIR $HOME/app
13
+
14
+ ENV VIRTUAL_ENV=$HOME/venv
15
+ RUN python3 -m venv $VIRTUAL_ENV
16
+ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
17
+ RUN pip install --no-cache-dir requests webdavclient3
18
+
19
+ COPY --chown=user . $HOME/app
20
+ COPY --chown=user sync_data.sh $HOME/app/
21
+
22
+ RUN chmod +x $HOME/app/cloudfilestoragemgr && \
23
+ chmod +x $HOME/app/sync_data.sh
24
+
25
+ RUN chown -R user:user /home/user
26
+ USER user
27
+
28
+ CMD ["/bin/bash", "-c", "$HOME/app/sync_data.sh & sleep 10 && ./cloudfilestoragemgr server"]
Dockerfile.ci ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM alpine:edge
2
+ ARG TARGETPLATFORM
3
+ LABEL MAINTAINER="i@nn.ci"
4
+ VOLUME /opt/alist/data/
5
+ WORKDIR /opt/alist/
6
+ COPY /${TARGETPLATFORM}/alist ./
7
+ COPY entrypoint.sh /entrypoint.sh
8
+ RUN apk update && \
9
+ apk upgrade --no-cache && \
10
+ apk add --no-cache bash ca-certificates su-exec tzdata; \
11
+ chmod +x /entrypoint.sh && \
12
+ rm -rf /var/cache/apk/* && \
13
+ /entrypoint.sh version
14
+ ENV PUID=0 PGID=0 UMASK=022
15
+ EXPOSE 5244 5245
16
+ CMD [ "/entrypoint.sh" ]
Dockerfile.ffmpeg ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ FROM ykxvk8yl5l/alist:latest
2
+ RUN apk update && \
3
+ apk add --no-cache ffmpeg \
4
+ rm -rf /var/cache/apk/*
LICENSE ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ GNU AFFERO GENERAL PUBLIC LICENSE
2
+ Version 3, 19 November 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU Affero General Public License is a free, copyleft license for
11
+ software and other kinds of works, specifically designed to ensure
12
+ cooperation with the community in the case of network server software.
13
+
14
+ The licenses for most software and other practical works are designed
15
+ to take away your freedom to share and change the works. By contrast,
16
+ our General Public Licenses are intended to guarantee your freedom to
17
+ share and change all versions of a program--to make sure it remains free
18
+ software for all its users.
19
+
20
+ When we speak of free software, we are referring to freedom, not
21
+ price. Our General Public Licenses are designed to make sure that you
22
+ have the freedom to distribute copies of free software (and charge for
23
+ them if you wish), that you receive source code or can get it if you
24
+ want it, that you can change the software or use pieces of it in new
25
+ free programs, and that you know you can do these things.
26
+
27
+ Developers that use our General Public Licenses protect your rights
28
+ with two steps: (1) assert copyright on the software, and (2) offer
29
+ you this License which gives you legal permission to copy, distribute
30
+ and/or modify the software.
31
+
32
+ A secondary benefit of defending all users' freedom is that
33
+ improvements made in alternate versions of the program, if they
34
+ receive widespread use, become available for other developers to
35
+ incorporate. Many developers of free software are heartened and
36
+ encouraged by the resulting cooperation. However, in the case of
37
+ software used on network servers, this result may fail to come about.
38
+ The GNU General Public License permits making a modified version and
39
+ letting the public access it on a server without ever releasing its
40
+ source code to the public.
41
+
42
+ The GNU Affero General Public License is designed specifically to
43
+ ensure that, in such cases, the modified source code becomes available
44
+ to the community. It requires the operator of a network server to
45
+ provide the source code of the modified version running there to the
46
+ users of that server. Therefore, public use of a modified version, on
47
+ a publicly accessible server, gives the public access to the source
48
+ code of the modified version.
49
+
50
+ An older license, called the Affero General Public License and
51
+ published by Affero, was designed to accomplish similar goals. This is
52
+ a different license, not a version of the Affero GPL, but Affero has
53
+ released a new version of the Affero GPL which permits relicensing under
54
+ this license.
55
+
56
+ The precise terms and conditions for copying, distribution and
57
+ modification follow.
58
+
59
+ TERMS AND CONDITIONS
60
+
61
+ 0. Definitions.
62
+
63
+ "This License" refers to version 3 of the GNU Affero General Public License.
64
+
65
+ "Copyright" also means copyright-like laws that apply to other kinds of
66
+ works, such as semiconductor masks.
67
+
68
+ "The Program" refers to any copyrightable work licensed under this
69
+ License. Each licensee is addressed as "you". "Licensees" and
70
+ "recipients" may be individuals or organizations.
71
+
72
+ To "modify" a work means to copy from or adapt all or part of the work
73
+ in a fashion requiring copyright permission, other than the making of an
74
+ exact copy. The resulting work is called a "modified version" of the
75
+ earlier work or a work "based on" the earlier work.
76
+
77
+ A "covered work" means either the unmodified Program or a work based
78
+ on the Program.
79
+
80
+ To "propagate" a work means to do anything with it that, without
81
+ permission, would make you directly or secondarily liable for
82
+ infringement under applicable copyright law, except executing it on a
83
+ computer or modifying a private copy. Propagation includes copying,
84
+ distribution (with or without modification), making available to the
85
+ public, and in some countries other activities as well.
86
+
87
+ To "convey" a work means any kind of propagation that enables other
88
+ parties to make or receive copies. Mere interaction with a user through
89
+ a computer network, with no transfer of a copy, is not conveying.
90
+
91
+ An interactive user interface displays "Appropriate Legal Notices"
92
+ to the extent that it includes a convenient and prominently visible
93
+ feature that (1) displays an appropriate copyright notice, and (2)
94
+ tells the user that there is no warranty for the work (except to the
95
+ extent that warranties are provided), that licensees may convey the
96
+ work under this License, and how to view a copy of this License. If
97
+ the interface presents a list of user commands or options, such as a
98
+ menu, a prominent item in the list meets this criterion.
99
+
100
+ 1. Source Code.
101
+
102
+ The "source code" for a work means the preferred form of the work
103
+ for making modifications to it. "Object code" means any non-source
104
+ form of a work.
105
+
106
+ A "Standard Interface" means an interface that either is an official
107
+ standard defined by a recognized standards body, or, in the case of
108
+ interfaces specified for a particular programming language, one that
109
+ is widely used among developers working in that language.
110
+
111
+ The "System Libraries" of an executable work include anything, other
112
+ than the work as a whole, that (a) is included in the normal form of
113
+ packaging a Major Component, but which is not part of that Major
114
+ Component, and (b) serves only to enable use of the work with that
115
+ Major Component, or to implement a Standard Interface for which an
116
+ implementation is available to the public in source code form. A
117
+ "Major Component", in this context, means a major essential component
118
+ (kernel, window system, and so on) of the specific operating system
119
+ (if any) on which the executable work runs, or a compiler used to
120
+ produce the work, or an object code interpreter used to run it.
121
+
122
+ The "Corresponding Source" for a work in object code form means all
123
+ the source code needed to generate, install, and (for an executable
124
+ work) run the object code and to modify the work, including scripts to
125
+ control those activities. However, it does not include the work's
126
+ System Libraries, or general-purpose tools or generally available free
127
+ programs which are used unmodified in performing those activities but
128
+ which are not part of the work. For example, Corresponding Source
129
+ includes interface definition files associated with source files for
130
+ the work, and the source code for shared libraries and dynamically
131
+ linked subprograms that the work is specifically designed to require,
132
+ such as by intimate data communication or control flow between those
133
+ subprograms and other parts of the work.
134
+
135
+ The Corresponding Source need not include anything that users
136
+ can regenerate automatically from other parts of the Corresponding
137
+ Source.
138
+
139
+ The Corresponding Source for a work in source code form is that
140
+ same work.
141
+
142
+ 2. Basic Permissions.
143
+
144
+ All rights granted under this License are granted for the term of
145
+ copyright on the Program, and are irrevocable provided the stated
146
+ conditions are met. This License explicitly affirms your unlimited
147
+ permission to run the unmodified Program. The output from running a
148
+ covered work is covered by this License only if the output, given its
149
+ content, constitutes a covered work. This License acknowledges your
150
+ rights of fair use or other equivalent, as provided by copyright law.
151
+
152
+ You may make, run and propagate covered works that you do not
153
+ convey, without conditions so long as your license otherwise remains
154
+ in force. You may convey covered works to others for the sole purpose
155
+ of having them make modifications exclusively for you, or provide you
156
+ with facilities for running those works, provided that you comply with
157
+ the terms of this License in conveying all material for which you do
158
+ not control copyright. Those thus making or running the covered works
159
+ for you must do so exclusively on your behalf, under your direction
160
+ and control, on terms that prohibit them from making any copies of
161
+ your copyrighted material outside their relationship with you.
162
+
163
+ Conveying under any other circumstances is permitted solely under
164
+ the conditions stated below. Sublicensing is not allowed; section 10
165
+ makes it unnecessary.
166
+
167
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168
+
169
+ No covered work shall be deemed part of an effective technological
170
+ measure under any applicable law fulfilling obligations under article
171
+ 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172
+ similar laws prohibiting or restricting circumvention of such
173
+ measures.
174
+
175
+ When you convey a covered work, you waive any legal power to forbid
176
+ circumvention of technological measures to the extent such circumvention
177
+ is effected by exercising rights under this License with respect to
178
+ the covered work, and you disclaim any intention to limit operation or
179
+ modification of the work as a means of enforcing, against the work's
180
+ users, your or third parties' legal rights to forbid circumvention of
181
+ technological measures.
182
+
183
+ 4. Conveying Verbatim Copies.
184
+
185
+ You may convey verbatim copies of the Program's source code as you
186
+ receive it, in any medium, provided that you conspicuously and
187
+ appropriately publish on each copy an appropriate copyright notice;
188
+ keep intact all notices stating that this License and any
189
+ non-permissive terms added in accord with section 7 apply to the code;
190
+ keep intact all notices of the absence of any warranty; and give all
191
+ recipients a copy of this License along with the Program.
192
+
193
+ You may charge any price or no price for each copy that you convey,
194
+ and you may offer support or warranty protection for a fee.
195
+
196
+ 5. Conveying Modified Source Versions.
197
+
198
+ You may convey a work based on the Program, or the modifications to
199
+ produce it from the Program, in the form of source code under the
200
+ terms of section 4, provided that you also meet all of these conditions:
201
+
202
+ a) The work must carry prominent notices stating that you modified
203
+ it, and giving a relevant date.
204
+
205
+ b) The work must carry prominent notices stating that it is
206
+ released under this License and any conditions added under section
207
+ 7. This requirement modifies the requirement in section 4 to
208
+ "keep intact all notices".
209
+
210
+ c) You must license the entire work, as a whole, under this
211
+ License to anyone who comes into possession of a copy. This
212
+ License will therefore apply, along with any applicable section 7
213
+ additional terms, to the whole of the work, and all its parts,
214
+ regardless of how they are packaged. This License gives no
215
+ permission to license the work in any other way, but it does not
216
+ invalidate such permission if you have separately received it.
217
+
218
+ d) If the work has interactive user interfaces, each must display
219
+ Appropriate Legal Notices; however, if the Program has interactive
220
+ interfaces that do not display Appropriate Legal Notices, your
221
+ work need not make them do so.
222
+
223
+ A compilation of a covered work with other separate and independent
224
+ works, which are not by their nature extensions of the covered work,
225
+ and which are not combined with it such as to form a larger program,
226
+ in or on a volume of a storage or distribution medium, is called an
227
+ "aggregate" if the compilation and its resulting copyright are not
228
+ used to limit the access or legal rights of the compilation's users
229
+ beyond what the individual works permit. Inclusion of a covered work
230
+ in an aggregate does not cause this License to apply to the other
231
+ parts of the aggregate.
232
+
233
+ 6. Conveying Non-Source Forms.
234
+
235
+ You may convey a covered work in object code form under the terms
236
+ of sections 4 and 5, provided that you also convey the
237
+ machine-readable Corresponding Source under the terms of this License,
238
+ in one of these ways:
239
+
240
+ a) Convey the object code in, or embodied in, a physical product
241
+ (including a physical distribution medium), accompanied by the
242
+ Corresponding Source fixed on a durable physical medium
243
+ customarily used for software interchange.
244
+
245
+ b) Convey the object code in, or embodied in, a physical product
246
+ (including a physical distribution medium), accompanied by a
247
+ written offer, valid for at least three years and valid for as
248
+ long as you offer spare parts or customer support for that product
249
+ model, to give anyone who possesses the object code either (1) a
250
+ copy of the Corresponding Source for all the software in the
251
+ product that is covered by this License, on a durable physical
252
+ medium customarily used for software interchange, for a price no
253
+ more than your reasonable cost of physically performing this
254
+ conveying of source, or (2) access to copy the
255
+ Corresponding Source from a network server at no charge.
256
+
257
+ c) Convey individual copies of the object code with a copy of the
258
+ written offer to provide the Corresponding Source. This
259
+ alternative is allowed only occasionally and noncommercially, and
260
+ only if you received the object code with such an offer, in accord
261
+ with subsection 6b.
262
+
263
+ d) Convey the object code by offering access from a designated
264
+ place (gratis or for a charge), and offer equivalent access to the
265
+ Corresponding Source in the same way through the same place at no
266
+ further charge. You need not require recipients to copy the
267
+ Corresponding Source along with the object code. If the place to
268
+ copy the object code is a network server, the Corresponding Source
269
+ may be on a different server (operated by you or a third party)
270
+ that supports equivalent copying facilities, provided you maintain
271
+ clear directions next to the object code saying where to find the
272
+ Corresponding Source. Regardless of what server hosts the
273
+ Corresponding Source, you remain obligated to ensure that it is
274
+ available for as long as needed to satisfy these requirements.
275
+
276
+ e) Convey the object code using peer-to-peer transmission, provided
277
+ you inform other peers where the object code and Corresponding
278
+ Source of the work are being offered to the general public at no
279
+ charge under subsection 6d.
280
+
281
+ A separable portion of the object code, whose source code is excluded
282
+ from the Corresponding Source as a System Library, need not be
283
+ included in conveying the object code work.
284
+
285
+ A "User Product" is either (1) a "consumer product", which means any
286
+ tangible personal property which is normally used for personal, family,
287
+ or household purposes, or (2) anything designed or sold for incorporation
288
+ into a dwelling. In determining whether a product is a consumer product,
289
+ doubtful cases shall be resolved in favor of coverage. For a particular
290
+ product received by a particular user, "normally used" refers to a
291
+ typical or common use of that class of product, regardless of the status
292
+ of the particular user or of the way in which the particular user
293
+ actually uses, or expects or is expected to use, the product. A product
294
+ is a consumer product regardless of whether the product has substantial
295
+ commercial, industrial or non-consumer uses, unless such uses represent
296
+ the only significant mode of use of the product.
297
+
298
+ "Installation Information" for a User Product means any methods,
299
+ procedures, authorization keys, or other information required to install
300
+ and execute modified versions of a covered work in that User Product from
301
+ a modified version of its Corresponding Source. The information must
302
+ suffice to ensure that the continued functioning of the modified object
303
+ code is in no case prevented or interfered with solely because
304
+ modification has been made.
305
+
306
+ If you convey an object code work under this section in, or with, or
307
+ specifically for use in, a User Product, and the conveying occurs as
308
+ part of a transaction in which the right of possession and use of the
309
+ User Product is transferred to the recipient in perpetuity or for a
310
+ fixed term (regardless of how the transaction is characterized), the
311
+ Corresponding Source conveyed under this section must be accompanied
312
+ by the Installation Information. But this requirement does not apply
313
+ if neither you nor any third party retains the ability to install
314
+ modified object code on the User Product (for example, the work has
315
+ been installed in ROM).
316
+
317
+ The requirement to provide Installation Information does not include a
318
+ requirement to continue to provide support service, warranty, or updates
319
+ for a work that has been modified or installed by the recipient, or for
320
+ the User Product in which it has been modified or installed. Access to a
321
+ network may be denied when the modification itself materially and
322
+ adversely affects the operation of the network or violates the rules and
323
+ protocols for communication across the network.
324
+
325
+ Corresponding Source conveyed, and Installation Information provided,
326
+ in accord with this section must be in a format that is publicly
327
+ documented (and with an implementation available to the public in
328
+ source code form), and must require no special password or key for
329
+ unpacking, reading or copying.
330
+
331
+ 7. Additional Terms.
332
+
333
+ "Additional permissions" are terms that supplement the terms of this
334
+ License by making exceptions from one or more of its conditions.
335
+ Additional permissions that are applicable to the entire Program shall
336
+ be treated as though they were included in this License, to the extent
337
+ that they are valid under applicable law. If additional permissions
338
+ apply only to part of the Program, that part may be used separately
339
+ under those permissions, but the entire Program remains governed by
340
+ this License without regard to the additional permissions.
341
+
342
+ When you convey a copy of a covered work, you may at your option
343
+ remove any additional permissions from that copy, or from any part of
344
+ it. (Additional permissions may be written to require their own
345
+ removal in certain cases when you modify the work.) You may place
346
+ additional permissions on material, added by you to a covered work,
347
+ for which you have or can give appropriate copyright permission.
348
+
349
+ Notwithstanding any other provision of this License, for material you
350
+ add to a covered work, you may (if authorized by the copyright holders of
351
+ that material) supplement the terms of this License with terms:
352
+
353
+ a) Disclaiming warranty or limiting liability differently from the
354
+ terms of sections 15 and 16 of this License; or
355
+
356
+ b) Requiring preservation of specified reasonable legal notices or
357
+ author attributions in that material or in the Appropriate Legal
358
+ Notices displayed by works containing it; or
359
+
360
+ c) Prohibiting misrepresentation of the origin of that material, or
361
+ requiring that modified versions of such material be marked in
362
+ reasonable ways as different from the original version; or
363
+
364
+ d) Limiting the use for publicity purposes of names of licensors or
365
+ authors of the material; or
366
+
367
+ e) Declining to grant rights under trademark law for use of some
368
+ trade names, trademarks, or service marks; or
369
+
370
+ f) Requiring indemnification of licensors and authors of that
371
+ material by anyone who conveys the material (or modified versions of
372
+ it) with contractual assumptions of liability to the recipient, for
373
+ any liability that these contractual assumptions directly impose on
374
+ those licensors and authors.
375
+
376
+ All other non-permissive additional terms are considered "further
377
+ restrictions" within the meaning of section 10. If the Program as you
378
+ received it, or any part of it, contains a notice stating that it is
379
+ governed by this License along with a term that is a further
380
+ restriction, you may remove that term. If a license document contains
381
+ a further restriction but permits relicensing or conveying under this
382
+ License, you may add to a covered work material governed by the terms
383
+ of that license document, provided that the further restriction does
384
+ not survive such relicensing or conveying.
385
+
386
+ If you add terms to a covered work in accord with this section, you
387
+ must place, in the relevant source files, a statement of the
388
+ additional terms that apply to those files, or a notice indicating
389
+ where to find the applicable terms.
390
+
391
+ Additional terms, permissive or non-permissive, may be stated in the
392
+ form of a separately written license, or stated as exceptions;
393
+ the above requirements apply either way.
394
+
395
+ 8. Termination.
396
+
397
+ You may not propagate or modify a covered work except as expressly
398
+ provided under this License. Any attempt otherwise to propagate or
399
+ modify it is void, and will automatically terminate your rights under
400
+ this License (including any patent licenses granted under the third
401
+ paragraph of section 11).
402
+
403
+ However, if you cease all violation of this License, then your
404
+ license from a particular copyright holder is reinstated (a)
405
+ provisionally, unless and until the copyright holder explicitly and
406
+ finally terminates your license, and (b) permanently, if the copyright
407
+ holder fails to notify you of the violation by some reasonable means
408
+ prior to 60 days after the cessation.
409
+
410
+ Moreover, your license from a particular copyright holder is
411
+ reinstated permanently if the copyright holder notifies you of the
412
+ violation by some reasonable means, this is the first time you have
413
+ received notice of violation of this License (for any work) from that
414
+ copyright holder, and you cure the violation prior to 30 days after
415
+ your receipt of the notice.
416
+
417
+ Termination of your rights under this section does not terminate the
418
+ licenses of parties who have received copies or rights from you under
419
+ this License. If your rights have been terminated and not permanently
420
+ reinstated, you do not qualify to receive new licenses for the same
421
+ material under section 10.
422
+
423
+ 9. Acceptance Not Required for Having Copies.
424
+
425
+ You are not required to accept this License in order to receive or
426
+ run a copy of the Program. Ancillary propagation of a covered work
427
+ occurring solely as a consequence of using peer-to-peer transmission
428
+ to receive a copy likewise does not require acceptance. However,
429
+ nothing other than this License grants you permission to propagate or
430
+ modify any covered work. These actions infringe copyright if you do
431
+ not accept this License. Therefore, by modifying or propagating a
432
+ covered work, you indicate your acceptance of this License to do so.
433
+
434
+ 10. Automatic Licensing of Downstream Recipients.
435
+
436
+ Each time you convey a covered work, the recipient automatically
437
+ receives a license from the original licensors, to run, modify and
438
+ propagate that work, subject to this License. You are not responsible
439
+ for enforcing compliance by third parties with this License.
440
+
441
+ An "entity transaction" is a transaction transferring control of an
442
+ organization, or substantially all assets of one, or subdividing an
443
+ organization, or merging organizations. If propagation of a covered
444
+ work results from an entity transaction, each party to that
445
+ transaction who receives a copy of the work also receives whatever
446
+ licenses to the work the party's predecessor in interest had or could
447
+ give under the previous paragraph, plus a right to possession of the
448
+ Corresponding Source of the work from the predecessor in interest, if
449
+ the predecessor has it or can get it with reasonable efforts.
450
+
451
+ You may not impose any further restrictions on the exercise of the
452
+ rights granted or affirmed under this License. For example, you may
453
+ not impose a license fee, royalty, or other charge for exercise of
454
+ rights granted under this License, and you may not initiate litigation
455
+ (including a cross-claim or counterclaim in a lawsuit) alleging that
456
+ any patent claim is infringed by making, using, selling, offering for
457
+ sale, or importing the Program or any portion of it.
458
+
459
+ 11. Patents.
460
+
461
+ A "contributor" is a copyright holder who authorizes use under this
462
+ License of the Program or a work on which the Program is based. The
463
+ work thus licensed is called the contributor's "contributor version".
464
+
465
+ A contributor's "essential patent claims" are all patent claims
466
+ owned or controlled by the contributor, whether already acquired or
467
+ hereafter acquired, that would be infringed by some manner, permitted
468
+ by this License, of making, using, or selling its contributor version,
469
+ but do not include claims that would be infringed only as a
470
+ consequence of further modification of the contributor version. For
471
+ purposes of this definition, "control" includes the right to grant
472
+ patent sublicenses in a manner consistent with the requirements of
473
+ this License.
474
+
475
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
476
+ patent license under the contributor's essential patent claims, to
477
+ make, use, sell, offer for sale, import and otherwise run, modify and
478
+ propagate the contents of its contributor version.
479
+
480
+ In the following three paragraphs, a "patent license" is any express
481
+ agreement or commitment, however denominated, not to enforce a patent
482
+ (such as an express permission to practice a patent or covenant not to
483
+ sue for patent infringement). To "grant" such a patent license to a
484
+ party means to make such an agreement or commitment not to enforce a
485
+ patent against the party.
486
+
487
+ If you convey a covered work, knowingly relying on a patent license,
488
+ and the Corresponding Source of the work is not available for anyone
489
+ to copy, free of charge and under the terms of this License, through a
490
+ publicly available network server or other readily accessible means,
491
+ then you must either (1) cause the Corresponding Source to be so
492
+ available, or (2) arrange to deprive yourself of the benefit of the
493
+ patent license for this particular work, or (3) arrange, in a manner
494
+ consistent with the requirements of this License, to extend the patent
495
+ license to downstream recipients. "Knowingly relying" means you have
496
+ actual knowledge that, but for the patent license, your conveying the
497
+ covered work in a country, or your recipient's use of the covered work
498
+ in a country, would infringe one or more identifiable patents in that
499
+ country that you have reason to believe are valid.
500
+
501
+ If, pursuant to or in connection with a single transaction or
502
+ arrangement, you convey, or propagate by procuring conveyance of, a
503
+ covered work, and grant a patent license to some of the parties
504
+ receiving the covered work authorizing them to use, propagate, modify
505
+ or convey a specific copy of the covered work, then the patent license
506
+ you grant is automatically extended to all recipients of the covered
507
+ work and works based on it.
508
+
509
+ A patent license is "discriminatory" if it does not include within
510
+ the scope of its coverage, prohibits the exercise of, or is
511
+ conditioned on the non-exercise of one or more of the rights that are
512
+ specifically granted under this License. You may not convey a covered
513
+ work if you are a party to an arrangement with a third party that is
514
+ in the business of distributing software, under which you make payment
515
+ to the third party based on the extent of your activity of conveying
516
+ the work, and under which the third party grants, to any of the
517
+ parties who would receive the covered work from you, a discriminatory
518
+ patent license (a) in connection with copies of the covered work
519
+ conveyed by you (or copies made from those copies), or (b) primarily
520
+ for and in connection with specific products or compilations that
521
+ contain the covered work, unless you entered into that arrangement,
522
+ or that patent license was granted, prior to 28 March 2007.
523
+
524
+ Nothing in this License shall be construed as excluding or limiting
525
+ any implied license or other defenses to infringement that may
526
+ otherwise be available to you under applicable patent law.
527
+
528
+ 12. No Surrender of Others' Freedom.
529
+
530
+ If conditions are imposed on you (whether by court order, agreement or
531
+ otherwise) that contradict the conditions of this License, they do not
532
+ excuse you from the conditions of this License. If you cannot convey a
533
+ covered work so as to satisfy simultaneously your obligations under this
534
+ License and any other pertinent obligations, then as a consequence you may
535
+ not convey it at all. For example, if you agree to terms that obligate you
536
+ to collect a royalty for further conveying from those to whom you convey
537
+ the Program, the only way you could satisfy both those terms and this
538
+ License would be to refrain entirely from conveying the Program.
539
+
540
+ 13. Remote Network Interaction; Use with the GNU General Public License.
541
+
542
+ Notwithstanding any other provision of this License, if you modify the
543
+ Program, your modified version must prominently offer all users
544
+ interacting with it remotely through a computer network (if your version
545
+ supports such interaction) an opportunity to receive the Corresponding
546
+ Source of your version by providing access to the Corresponding Source
547
+ from a network server at no charge, through some standard or customary
548
+ means of facilitating copying of software. This Corresponding Source
549
+ shall include the Corresponding Source for any work covered by version 3
550
+ of the GNU General Public License that is incorporated pursuant to the
551
+ following paragraph.
552
+
553
+ Notwithstanding any other provision of this License, you have
554
+ permission to link or combine any covered work with a work licensed
555
+ under version 3 of the GNU General Public License into a single
556
+ combined work, and to convey the resulting work. The terms of this
557
+ License will continue to apply to the part which is the covered work,
558
+ but the work with which it is combined will remain governed by version
559
+ 3 of the GNU General Public License.
560
+
561
+ 14. Revised Versions of this License.
562
+
563
+ The Free Software Foundation may publish revised and/or new versions of
564
+ the GNU Affero General Public License from time to time. Such new versions
565
+ will be similar in spirit to the present version, but may differ in detail to
566
+ address new problems or concerns.
567
+
568
+ Each version is given a distinguishing version number. If the
569
+ Program specifies that a certain numbered version of the GNU Affero General
570
+ Public License "or any later version" applies to it, you have the
571
+ option of following the terms and conditions either of that numbered
572
+ version or of any later version published by the Free Software
573
+ Foundation. If the Program does not specify a version number of the
574
+ GNU Affero General Public License, you may choose any version ever published
575
+ by the Free Software Foundation.
576
+
577
+ If the Program specifies that a proxy can decide which future
578
+ versions of the GNU Affero General Public License can be used, that proxy's
579
+ public statement of acceptance of a version permanently authorizes you
580
+ to choose that version for the Program.
581
+
582
+ Later license versions may give you additional or different
583
+ permissions. However, no additional obligations are imposed on any
584
+ author or copyright holder as a result of your choosing to follow a
585
+ later version.
586
+
587
+ 15. Disclaimer of Warranty.
588
+
589
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590
+ APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591
+ HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592
+ OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593
+ THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594
+ PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595
+ IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596
+ ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597
+
598
+ 16. Limitation of Liability.
599
+
600
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601
+ WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602
+ THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603
+ GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604
+ USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605
+ DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606
+ PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607
+ EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608
+ SUCH DAMAGES.
609
+
610
+ 17. Interpretation of Sections 15 and 16.
611
+
612
+ If the disclaimer of warranty and limitation of liability provided
613
+ above cannot be given local legal effect according to their terms,
614
+ reviewing courts shall apply local law that most closely approximates
615
+ an absolute waiver of all civil liability in connection with the
616
+ Program, unless a warranty or assumption of liability accompanies a
617
+ copy of the Program in return for a fee.
618
+
619
+ END OF TERMS AND CONDITIONS
620
+
621
+ How to Apply These Terms to Your New Programs
622
+
623
+ If you develop a new program, and you want it to be of the greatest
624
+ possible use to the public, the best way to achieve this is to make it
625
+ free software which everyone can redistribute and change under these terms.
626
+
627
+ To do so, attach the following notices to the program. It is safest
628
+ to attach them to the start of each source file to most effectively
629
+ state the exclusion of warranty; and each file should have at least
630
+ the "copyright" line and a pointer to where the full notice is found.
631
+
632
+ <one line to give the program's name and a brief idea of what it does.>
633
+ Copyright (C) <year> <name of author>
634
+
635
+ This program is free software: you can redistribute it and/or modify
636
+ it under the terms of the GNU Affero General Public License as published
637
+ by the Free Software Foundation, either version 3 of the License, or
638
+ (at your option) any later version.
639
+
640
+ This program is distributed in the hope that it will be useful,
641
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
642
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643
+ GNU Affero General Public License for more details.
644
+
645
+ You should have received a copy of the GNU Affero General Public License
646
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
647
+
648
+ Also add information on how to contact you by electronic and paper mail.
649
+
650
+ If your software can interact with users remotely through a computer
651
+ network, you should also make sure that it provides a way for users to
652
+ get its source. For example, if your program is a web application, its
653
+ interface could display a "Source" link that leads users to an archive
654
+ of the code. There are many ways you could offer source, and different
655
+ solutions will be better for different programs; see section 13 for the
656
+ specific requirements.
657
+
658
+ You should also get your employer (if you work as a programmer) or school,
659
+ if any, to sign a "copyright disclaimer" for the program, if necessary.
660
+ For more information on this, and how to apply and follow the GNU AGPL, see
661
+ <https://www.gnu.org/licenses/>.
alwaysdata.sh ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Declare site in YAML, as documented on the documentation: https://help.alwaysdata.com/en/marketplace/build-application-script/
4
+ # site:
5
+ # type: user_program
6
+ # working_directory: '{INSTALL_PATH_RELATIVE}'
7
+ # command: './alist server'
8
+ # requirements:
9
+ # disk: 30
10
+ # form:
11
+ # password:
12
+ # type: password
13
+ # label:
14
+ # en: Password
15
+ # fr: Mot de passe
16
+ # max_length: 255
17
+
18
+ set -e
19
+ cd $INSTALL_PATH
20
+ wget -O- --no-hsts https://github.com/ykxVK8yL5L/alist/releases/download/latest/alist-linux-amd64.tar.gz | tar -xz --strip-components=0
21
+
22
+ ./alist admin set $FORM_PASSWORD
23
+ sed -i "s/5244/$PORT/g" data/config.json
build-freebsd.sh ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ appName="alist"
4
+ builtAt="$(date +'%F %T %z')"
5
+ goVersion=$(go version | sed 's/go version //')
6
+ gitAuthor="Xhofe <i@nn.ci>"
7
+ gitCommit=$(git log --pretty=format:"%h" -1)
8
+ version=$(git describe --long --tags --dirty --always)
9
+ webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/ykxVK8yL5L/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
10
+
11
+ ldflags="\
12
+ -w -s \
13
+ -X 'github.com/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \
14
+ -X 'github.com/alist-org/alist/v3/internal/conf.GoVersion=$goVersion' \
15
+ -X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \
16
+ -X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \
17
+ -X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \
18
+ -X 'github.com/alist-org/alist/v3/internal/conf.WebVersion=$webVersion' \
19
+ "
20
+
21
+ go build -ldflags="$ldflags" -tags=jsoniter .
build.sh ADDED
@@ -0,0 +1,294 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ appName="alist"
2
+ builtAt="$(date +'%F %T %z')"
3
+ goVersion=$(go version | sed 's/go version //')
4
+ gitAuthor="Xhofe <i@nn.ci>"
5
+ gitCommit=$(git log --pretty=format:"%h" -1)
6
+
7
+ if [ "$1" = "dev" ]; then
8
+ version="dev"
9
+ webVersion="dev"
10
+ else
11
+ version=$(git describe --abbrev=0 --tags)
12
+ webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/ykxVK8yL5L/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
13
+ fi
14
+
15
+ echo "backend version: $version"
16
+ echo "frontend version: $webVersion"
17
+
18
+ ldflags="\
19
+ -w -s \
20
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.BuiltAt=$builtAt' \
21
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.GoVersion=$goVersion' \
22
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.GitAuthor=$gitAuthor' \
23
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.GitCommit=$gitCommit' \
24
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.Version=$version' \
25
+ -X 'github.com/ykxVK8yL5L/alist/v3/internal/conf.WebVersion=$webVersion' \
26
+ "
27
+
28
+ FetchWebDev() {
29
+ curl -L https://github.com/ykxVK8yL5L/alist-web/releases/latest/download/dist.tar.gz -o web-dist-dev.tar.gz
30
+ tar -zxvf web-dist-dev.tar.gz
31
+ rm -rf public/dist
32
+ mv -f web-dist-dev/dist public
33
+ rm -rf web-dist-dev web-dist-dev.tar.gz
34
+ }
35
+
36
+ FetchWebRelease() {
37
+ curl -L https://github.com/ykxVK8yL5L/alist-web/releases/latest/download/dist.tar.gz -o dist.tar.gz
38
+ tar -zxvf dist.tar.gz
39
+ rm -rf public/dist
40
+ mv -f dist public
41
+ rm -rf dist.tar.gz
42
+ }
43
+
44
+ BuildWinArm64() {
45
+ echo building for windows-arm64
46
+ chmod +x ./wrapper/zcc-arm64
47
+ chmod +x ./wrapper/zcxx-arm64
48
+ export GOOS=windows
49
+ export GOARCH=arm64
50
+ export CC=$(pwd)/wrapper/zcc-arm64
51
+ export CXX=$(pwd)/wrapper/zcxx-arm64
52
+ export CGO_ENABLED=1
53
+ go build -o "$1" -ldflags="$ldflags" -tags=jsoniter .
54
+ }
55
+
56
+ BuildDev() {
57
+ rm -rf .git/
58
+ mkdir -p "dist"
59
+ muslflags="--extldflags '-static -fpic' $ldflags"
60
+ BASE="https://musl.nn.ci/"
61
+ FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross)
62
+ for i in "${FILES[@]}"; do
63
+ url="${BASE}${i}.tgz"
64
+ curl -L -o "${i}.tgz" "${url}"
65
+ sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
66
+ done
67
+ OS_ARCHES=(linux-musl-amd64 linux-musl-arm64)
68
+ CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc)
69
+ for i in "${!OS_ARCHES[@]}"; do
70
+ os_arch=${OS_ARCHES[$i]}
71
+ cgo_cc=${CGO_ARGS[$i]}
72
+ echo building for ${os_arch}
73
+ export GOOS=${os_arch%%-*}
74
+ export GOARCH=${os_arch##*-}
75
+ export CC=${cgo_cc}
76
+ export CGO_ENABLED=1
77
+ go build -o ./dist/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
78
+ done
79
+ xgo -targets=windows/amd64,darwin/amd64,darwin/arm64 -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
80
+ mv alist-* dist
81
+ cd dist
82
+ cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
83
+ upx -9 ./alist-windows-amd64-upx.exe
84
+ find . -type f -print0 | xargs -0 md5sum >md5.txt
85
+ cat md5.txt
86
+ }
87
+
88
+ PrepareBuildDocker() {
89
+ echo "replace github.com/mattn/go-sqlite3 => github.com/leso-kn/go-sqlite3 v0.0.0-20230710125852-03158dc838ed" >>go.mod
90
+ go get gorm.io/driver/sqlite@v1.4.4
91
+ go mod download
92
+ }
93
+
94
+ BuildDocker() {
95
+ PrepareBuildDocker
96
+ go build -o ./bin/alist -ldflags="$ldflags" -tags=jsoniter .
97
+ }
98
+
99
+ BuildDockerMultiplatform() {
100
+ PrepareBuildDocker
101
+
102
+ BASE="https://musl.cc/"
103
+ FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross i486-linux-musl-cross s390x-linux-musl-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross)
104
+ for i in "${FILES[@]}"; do
105
+ url="${BASE}${i}.tgz"
106
+ curl -L -o "${i}.tgz" "${url}"
107
+ sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
108
+ rm -f "${i}.tgz"
109
+ done
110
+
111
+ docker_lflags="--extldflags '-static -fpic' $ldflags"
112
+ export CGO_ENABLED=1
113
+
114
+ OS_ARCHES=(linux-amd64 linux-arm64 linux-386 linux-s390x)
115
+ CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc i486-linux-musl-gcc s390x-linux-musl-gcc)
116
+ for i in "${!OS_ARCHES[@]}"; do
117
+ os_arch=${OS_ARCHES[$i]}
118
+ cgo_cc=${CGO_ARGS[$i]}
119
+ os=${os_arch%%-*}
120
+ arch=${os_arch##*-}
121
+ export GOOS=$os
122
+ export GOARCH=$arch
123
+ export CC=${cgo_cc}
124
+ echo "building for $os_arch"
125
+ go build -o ./$os/$arch/alist -ldflags="$docker_lflags" -tags=jsoniter .
126
+ done
127
+
128
+ DOCKER_ARM_ARCHES=(linux-arm/v6 linux-arm/v7)
129
+ CGO_ARGS=(armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc)
130
+ GO_ARM=(6 7)
131
+ export GOOS=linux
132
+ export GOARCH=arm
133
+ for i in "${!DOCKER_ARM_ARCHES[@]}"; do
134
+ docker_arch=${DOCKER_ARM_ARCHES[$i]}
135
+ cgo_cc=${CGO_ARGS[$i]}
136
+ export GOARM=${GO_ARM[$i]}
137
+ export CC=${cgo_cc}
138
+ echo "building for $docker_arch"
139
+ go build -o ./${docker_arch%%-*}/${docker_arch##*-}/alist -ldflags="$docker_lflags" -tags=jsoniter .
140
+ done
141
+ }
142
+
143
+ BuildRelease() {
144
+ rm -rf .git/
145
+ mkdir -p "build"
146
+ BuildWinArm64 ./build/alist-windows-arm64.exe
147
+ xgo -out "$appName" -ldflags="$ldflags" -tags=jsoniter .
148
+ # why? Because some target platforms seem to have issues with upx compression
149
+ upx -9 ./alist-linux-amd64
150
+ cp ./alist-windows-amd64.exe ./alist-windows-amd64-upx.exe
151
+ upx -9 ./alist-windows-amd64-upx.exe
152
+ mv alist-* build
153
+ }
154
+
155
+ BuildReleaseLinuxMusl() {
156
+ rm -rf .git/
157
+ mkdir -p "build"
158
+ muslflags="--extldflags '-static -fpic' $ldflags"
159
+ BASE="https://musl.nn.ci/"
160
+ FILES=(x86_64-linux-musl-cross aarch64-linux-musl-cross mips-linux-musl-cross mips64-linux-musl-cross mips64el-linux-musl-cross mipsel-linux-musl-cross powerpc64le-linux-musl-cross s390x-linux-musl-cross)
161
+ for i in "${FILES[@]}"; do
162
+ url="${BASE}${i}.tgz"
163
+ curl -L -o "${i}.tgz" "${url}"
164
+ sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
165
+ rm -f "${i}.tgz"
166
+ done
167
+ OS_ARCHES=(linux-musl-amd64 linux-musl-arm64 linux-musl-mips linux-musl-mips64 linux-musl-mips64le linux-musl-mipsle linux-musl-ppc64le linux-musl-s390x)
168
+ CGO_ARGS=(x86_64-linux-musl-gcc aarch64-linux-musl-gcc mips-linux-musl-gcc mips64-linux-musl-gcc mips64el-linux-musl-gcc mipsel-linux-musl-gcc powerpc64le-linux-musl-gcc s390x-linux-musl-gcc)
169
+ for i in "${!OS_ARCHES[@]}"; do
170
+ os_arch=${OS_ARCHES[$i]}
171
+ cgo_cc=${CGO_ARGS[$i]}
172
+ echo building for ${os_arch}
173
+ export GOOS=${os_arch%%-*}
174
+ export GOARCH=${os_arch##*-}
175
+ export CC=${cgo_cc}
176
+ export CGO_ENABLED=1
177
+ go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
178
+ done
179
+ }
180
+
181
+ BuildReleaseLinuxMuslArm() {
182
+ rm -rf .git/
183
+ mkdir -p "build"
184
+ muslflags="--extldflags '-static -fpic' $ldflags"
185
+ BASE="https://musl.nn.ci/"
186
+ # FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armeb-linux-musleabi-cross armeb-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)
187
+ FILES=(arm-linux-musleabi-cross arm-linux-musleabihf-cross armel-linux-musleabi-cross armel-linux-musleabihf-cross armv5l-linux-musleabi-cross armv5l-linux-musleabihf-cross armv6-linux-musleabi-cross armv6-linux-musleabihf-cross armv7l-linux-musleabihf-cross armv7m-linux-musleabi-cross armv7r-linux-musleabihf-cross)
188
+ for i in "${FILES[@]}"; do
189
+ url="${BASE}${i}.tgz"
190
+ curl -L -o "${i}.tgz" "${url}"
191
+ sudo tar xf "${i}.tgz" --strip-components 1 -C /usr/local
192
+ rm -f "${i}.tgz"
193
+ done
194
+ # OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armeb linux-musleabihf-armeb linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r)
195
+ # CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armeb-linux-musleabi-gcc armeb-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc)
196
+ # GOARMS=('' '' '' '' '' '' '5' '5' '6' '6' '7' '7' '7')
197
+ OS_ARCHES=(linux-musleabi-arm linux-musleabihf-arm linux-musleabi-armel linux-musleabihf-armel linux-musleabi-armv5l linux-musleabihf-armv5l linux-musleabi-armv6 linux-musleabihf-armv6 linux-musleabihf-armv7l linux-musleabi-armv7m linux-musleabihf-armv7r)
198
+ CGO_ARGS=(arm-linux-musleabi-gcc arm-linux-musleabihf-gcc armel-linux-musleabi-gcc armel-linux-musleabihf-gcc armv5l-linux-musleabi-gcc armv5l-linux-musleabihf-gcc armv6-linux-musleabi-gcc armv6-linux-musleabihf-gcc armv7l-linux-musleabihf-gcc armv7m-linux-musleabi-gcc armv7r-linux-musleabihf-gcc)
199
+ GOARMS=('' '' '' '' '5' '5' '6' '6' '7' '7' '7')
200
+ for i in "${!OS_ARCHES[@]}"; do
201
+ os_arch=${OS_ARCHES[$i]}
202
+ cgo_cc=${CGO_ARGS[$i]}
203
+ arm=${GOARMS[$i]}
204
+ echo building for ${os_arch}
205
+ export GOOS=linux
206
+ export GOARCH=arm
207
+ export CC=${cgo_cc}
208
+ export CGO_ENABLED=1
209
+ export GOARM=${arm}
210
+ go build -o ./build/$appName-$os_arch -ldflags="$muslflags" -tags=jsoniter .
211
+ done
212
+ }
213
+
214
+ BuildReleaseAndroid() {
215
+ rm -rf .git/
216
+ mkdir -p "build"
217
+ wget https://dl.google.com/android/repository/android-ndk-r26b-linux.zip
218
+ unzip android-ndk-r26b-linux.zip
219
+ rm android-ndk-r26b-linux.zip
220
+ OS_ARCHES=(amd64 arm64 386 arm)
221
+ CGO_ARGS=(x86_64-linux-android24-clang aarch64-linux-android24-clang i686-linux-android24-clang armv7a-linux-androideabi24-clang)
222
+ for i in "${!OS_ARCHES[@]}"; do
223
+ os_arch=${OS_ARCHES[$i]}
224
+ cgo_cc=$(realpath android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/${CGO_ARGS[$i]})
225
+ echo building for android-${os_arch}
226
+ export GOOS=android
227
+ export GOARCH=${os_arch##*-}
228
+ export CC=${cgo_cc}
229
+ export CGO_ENABLED=1
230
+ go build -o ./build/$appName-android-$os_arch -ldflags="$ldflags" -tags=jsoniter .
231
+ android-ndk-r26b/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-strip ./build/$appName-android-$os_arch
232
+ done
233
+ }
234
+
235
+ MakeRelease() {
236
+ cd build
237
+ mkdir compress
238
+ for i in $(find . -type f -name "$appName-linux-*"); do
239
+ cp "$i" alist
240
+ tar -czvf compress/"$i".tar.gz alist
241
+ rm -f alist
242
+ done
243
+ for i in $(find . -type f -name "$appName-android-*"); do
244
+ cp "$i" alist
245
+ tar -czvf compress/"$i".tar.gz alist
246
+ rm -f alist
247
+ done
248
+ for i in $(find . -type f -name "$appName-darwin-*"); do
249
+ cp "$i" alist
250
+ tar -czvf compress/"$i".tar.gz alist
251
+ rm -f alist
252
+ done
253
+ for i in $(find . -type f -name "$appName-windows-*"); do
254
+ cp "$i" alist.exe
255
+ zip compress/$(echo $i | sed 's/\.[^.]*$//').zip alist.exe
256
+ rm -f alist.exe
257
+ done
258
+ cd compress
259
+ find . -type f -print0 | xargs -0 md5sum >"$1"
260
+ cat "$1"
261
+ cd ../..
262
+ }
263
+
264
+ if [ "$1" = "dev" ]; then
265
+ FetchWebDev
266
+ if [ "$2" = "docker" ]; then
267
+ BuildDocker
268
+ elif [ "$2" = "docker-multiplatform" ]; then
269
+ BuildDockerMultiplatform
270
+ else
271
+ BuildDev
272
+ fi
273
+ elif [ "$1" = "release" ]; then
274
+ FetchWebRelease
275
+ if [ "$2" = "docker" ]; then
276
+ BuildDocker
277
+ elif [ "$2" = "docker-multiplatform" ]; then
278
+ BuildDockerMultiplatform
279
+ elif [ "$2" = "linux_musl_arm" ]; then
280
+ BuildReleaseLinuxMuslArm
281
+ MakeRelease "md5-linux-musl-arm.txt"
282
+ elif [ "$2" = "linux_musl" ]; then
283
+ BuildReleaseLinuxMusl
284
+ MakeRelease "md5-linux-musl.txt"
285
+ elif [ "$2" = "android" ]; then
286
+ BuildReleaseAndroid
287
+ MakeRelease "md5-android.txt"
288
+ else
289
+ BuildRelease
290
+ MakeRelease "md5.txt"
291
+ fi
292
+ else
293
+ echo -e "Parameter error"
294
+ fi
cmd/admin.go ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/alist-org/alist/v3/internal/conf"
8
+ "github.com/alist-org/alist/v3/internal/op"
9
+ "github.com/alist-org/alist/v3/internal/setting"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "github.com/alist-org/alist/v3/pkg/utils/random"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ // AdminCmd represents the password command
16
+ var AdminCmd = &cobra.Command{
17
+ Use: "admin",
18
+ Aliases: []string{"password"},
19
+ Short: "Show admin user's info and some operations about admin user's password",
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ Init()
22
+ defer Release()
23
+ admin, err := op.GetAdmin()
24
+ if err != nil {
25
+ utils.Log.Errorf("failed get admin user: %+v", err)
26
+ } else {
27
+ utils.Log.Infof("Admin user's username: %s", admin.Username)
28
+ utils.Log.Infof("The password can only be output at the first startup, and then stored as a hash value, which cannot be reversed")
29
+ utils.Log.Infof("You can reset the password with a random string by running [alist admin random]")
30
+ utils.Log.Infof("You can also set a new password by running [alist admin set NEW_PASSWORD]")
31
+ }
32
+ },
33
+ }
34
+
35
+ var RandomPasswordCmd = &cobra.Command{
36
+ Use: "random",
37
+ Short: "Reset admin user's password to a random string",
38
+ Run: func(cmd *cobra.Command, args []string) {
39
+ newPwd := random.String(8)
40
+ setAdminPassword(newPwd)
41
+ },
42
+ }
43
+
44
+ var SetPasswordCmd = &cobra.Command{
45
+ Use: "set",
46
+ Short: "Set admin user's password",
47
+ Run: func(cmd *cobra.Command, args []string) {
48
+ if len(args) == 0 {
49
+ utils.Log.Errorf("Please enter the new password")
50
+ return
51
+ }
52
+ setAdminPassword(args[0])
53
+ },
54
+ }
55
+
56
+ var ShowTokenCmd = &cobra.Command{
57
+ Use: "token",
58
+ Short: "Show admin token",
59
+ Run: func(cmd *cobra.Command, args []string) {
60
+ Init()
61
+ defer Release()
62
+ token := setting.GetStr(conf.Token)
63
+ utils.Log.Infof("Admin token: %s", token)
64
+ },
65
+ }
66
+
67
+ func setAdminPassword(pwd string) {
68
+ Init()
69
+ defer Release()
70
+ admin, err := op.GetAdmin()
71
+ if err != nil {
72
+ utils.Log.Errorf("failed get admin user: %+v", err)
73
+ return
74
+ }
75
+ admin.SetPassword(pwd)
76
+ if err := op.UpdateUser(admin); err != nil {
77
+ utils.Log.Errorf("failed update admin user: %+v", err)
78
+ return
79
+ }
80
+ utils.Log.Infof("admin user has been updated:")
81
+ utils.Log.Infof("username: %s", admin.Username)
82
+ utils.Log.Infof("password: %s", pwd)
83
+ DelAdminCacheOnline()
84
+ }
85
+
86
+ func init() {
87
+ RootCmd.AddCommand(AdminCmd)
88
+ AdminCmd.AddCommand(RandomPasswordCmd)
89
+ AdminCmd.AddCommand(SetPasswordCmd)
90
+ AdminCmd.AddCommand(ShowTokenCmd)
91
+ // Here you will define your flags and configuration settings.
92
+
93
+ // Cobra supports Persistent Flags which will work for this command
94
+ // and all subcommands, e.g.:
95
+ // passwordCmd.PersistentFlags().String("foo", "", "A help for foo")
96
+
97
+ // Cobra supports local flags which will only run when this command
98
+ // is called directly, e.g.:
99
+ // passwordCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
100
+ }
cmd/cancel2FA.go ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/alist-org/alist/v3/internal/op"
8
+ "github.com/alist-org/alist/v3/pkg/utils"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ // Cancel2FACmd represents the delete2fa command
13
+ var Cancel2FACmd = &cobra.Command{
14
+ Use: "cancel2fa",
15
+ Short: "Delete 2FA of admin user",
16
+ Run: func(cmd *cobra.Command, args []string) {
17
+ Init()
18
+ defer Release()
19
+ admin, err := op.GetAdmin()
20
+ if err != nil {
21
+ utils.Log.Errorf("failed to get admin user: %+v", err)
22
+ } else {
23
+ err := op.Cancel2FAByUser(admin)
24
+ if err != nil {
25
+ utils.Log.Errorf("failed to cancel 2FA: %+v", err)
26
+ } else {
27
+ utils.Log.Info("2FA canceled")
28
+ DelAdminCacheOnline()
29
+ }
30
+ }
31
+ },
32
+ }
33
+
34
+ func init() {
35
+ RootCmd.AddCommand(Cancel2FACmd)
36
+
37
+ // Here you will define your flags and configuration settings.
38
+
39
+ // Cobra supports Persistent Flags which will work for this command
40
+ // and all subcommands, e.g.:
41
+ // cancel2FACmd.PersistentFlags().String("foo", "", "A help for foo")
42
+
43
+ // Cobra supports local flags which will only run when this command
44
+ // is called directly, e.g.:
45
+ // cancel2FACmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
46
+ }
cmd/common.go ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "os"
5
+ "path/filepath"
6
+ "strconv"
7
+
8
+ "github.com/alist-org/alist/v3/internal/bootstrap"
9
+ "github.com/alist-org/alist/v3/internal/bootstrap/data"
10
+ "github.com/alist-org/alist/v3/internal/db"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ log "github.com/sirupsen/logrus"
13
+ )
14
+
15
+ func Init() {
16
+ bootstrap.InitConfig()
17
+ bootstrap.Log()
18
+ bootstrap.InitDB()
19
+ data.InitData()
20
+ bootstrap.InitIndex()
21
+ }
22
+
23
+ func Release() {
24
+ db.Close()
25
+ }
26
+
27
+ var pid = -1
28
+ var pidFile string
29
+
30
+ func initDaemon() {
31
+ ex, err := os.Executable()
32
+ if err != nil {
33
+ log.Fatal(err)
34
+ }
35
+ exPath := filepath.Dir(ex)
36
+ _ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
37
+ pidFile = filepath.Join(exPath, "daemon/pid")
38
+ if utils.Exists(pidFile) {
39
+ bytes, err := os.ReadFile(pidFile)
40
+ if err != nil {
41
+ log.Fatal("failed to read pid file", err)
42
+ }
43
+ id, err := strconv.Atoi(string(bytes))
44
+ if err != nil {
45
+ log.Fatal("failed to parse pid data", err)
46
+ }
47
+ pid = id
48
+ }
49
+ }
cmd/flags/config.go ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package flags
2
+
3
+ var (
4
+ DataDir string
5
+ Debug bool
6
+ NoPrefix bool
7
+ Dev bool
8
+ ForceBinDir bool
9
+ LogStd bool
10
+ )
cmd/lang.go ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Package cmd
3
+ Copyright © 2022 Noah Hsu<i@nn.ci>
4
+ */
5
+ package cmd
6
+
7
+ import (
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "reflect"
12
+ "strings"
13
+
14
+ _ "github.com/alist-org/alist/v3/drivers"
15
+ "github.com/alist-org/alist/v3/internal/bootstrap/data"
16
+ "github.com/alist-org/alist/v3/internal/conf"
17
+ "github.com/alist-org/alist/v3/internal/op"
18
+ "github.com/alist-org/alist/v3/pkg/utils"
19
+ log "github.com/sirupsen/logrus"
20
+ "github.com/spf13/cobra"
21
+ )
22
+
23
+ type KV[V any] map[string]V
24
+
25
+ type Drivers KV[KV[interface{}]]
26
+
27
+ func firstUpper(s string) string {
28
+ if s == "" {
29
+ return ""
30
+ }
31
+ return strings.ToUpper(s[:1]) + s[1:]
32
+ }
33
+
34
+ func convert(s string) string {
35
+ ss := strings.Split(s, "_")
36
+ ans := strings.Join(ss, " ")
37
+ return firstUpper(ans)
38
+ }
39
+
40
+ func writeFile(name string, data interface{}) {
41
+ f, err := os.Open(fmt.Sprintf("../alist-web/src/lang/en/%s.json", name))
42
+ if err != nil {
43
+ log.Errorf("failed to open %s.json: %+v", name, err)
44
+ return
45
+ }
46
+ defer f.Close()
47
+ content, err := io.ReadAll(f)
48
+ if err != nil {
49
+ log.Errorf("failed to read %s.json: %+v", name, err)
50
+ return
51
+ }
52
+ oldData := make(map[string]interface{})
53
+ newData := make(map[string]interface{})
54
+ err = utils.Json.Unmarshal(content, &oldData)
55
+ if err != nil {
56
+ log.Errorf("failed to unmarshal %s.json: %+v", name, err)
57
+ return
58
+ }
59
+ content, err = utils.Json.Marshal(data)
60
+ if err != nil {
61
+ log.Errorf("failed to marshal json: %+v", err)
62
+ return
63
+ }
64
+ err = utils.Json.Unmarshal(content, &newData)
65
+ if err != nil {
66
+ log.Errorf("failed to unmarshal json: %+v", err)
67
+ return
68
+ }
69
+ if reflect.DeepEqual(oldData, newData) {
70
+ log.Infof("%s.json no changed, skip", name)
71
+ } else {
72
+ log.Infof("%s.json changed, update file", name)
73
+ //log.Infof("old: %+v\nnew:%+v", oldData, data)
74
+ utils.WriteJsonToFile(fmt.Sprintf("lang/%s.json", name), newData, true)
75
+ }
76
+ }
77
+
78
+ func generateDriversJson() {
79
+ drivers := make(Drivers)
80
+ drivers["drivers"] = make(KV[interface{}])
81
+ drivers["config"] = make(KV[interface{}])
82
+ driverInfoMap := op.GetDriverInfoMap()
83
+ for k, v := range driverInfoMap {
84
+ drivers["drivers"][k] = convert(k)
85
+ items := make(KV[interface{}])
86
+ config := map[string]string{}
87
+ if v.Config.Alert != "" {
88
+ alert := strings.SplitN(v.Config.Alert, "|", 2)
89
+ if len(alert) > 1 {
90
+ config["alert"] = alert[1]
91
+ }
92
+ }
93
+ drivers["config"][k] = config
94
+ for i := range v.Additional {
95
+ item := v.Additional[i]
96
+ items[item.Name] = convert(item.Name)
97
+ if item.Help != "" {
98
+ items[fmt.Sprintf("%s-tips", item.Name)] = item.Help
99
+ }
100
+ if item.Type == conf.TypeSelect && len(item.Options) > 0 {
101
+ options := make(KV[string])
102
+ _options := strings.Split(item.Options, ",")
103
+ for _, o := range _options {
104
+ options[o] = convert(o)
105
+ }
106
+ items[fmt.Sprintf("%ss", item.Name)] = options
107
+ }
108
+ }
109
+ drivers[k] = items
110
+ }
111
+ writeFile("drivers", drivers)
112
+ }
113
+
114
+ func generateSettingsJson() {
115
+ settings := data.InitialSettings()
116
+ settingsLang := make(KV[any])
117
+ for _, setting := range settings {
118
+ settingsLang[setting.Key] = convert(setting.Key)
119
+ if setting.Help != "" {
120
+ settingsLang[fmt.Sprintf("%s-tips", setting.Key)] = setting.Help
121
+ }
122
+ if setting.Type == conf.TypeSelect && len(setting.Options) > 0 {
123
+ options := make(KV[string])
124
+ _options := strings.Split(setting.Options, ",")
125
+ for _, o := range _options {
126
+ options[o] = convert(o)
127
+ }
128
+ settingsLang[fmt.Sprintf("%ss", setting.Key)] = options
129
+ }
130
+ }
131
+ writeFile("settings", settingsLang)
132
+ //utils.WriteJsonToFile("lang/settings.json", settingsLang)
133
+ }
134
+
135
+ // LangCmd represents the lang command
136
+ var LangCmd = &cobra.Command{
137
+ Use: "lang",
138
+ Short: "Generate language json file",
139
+ Run: func(cmd *cobra.Command, args []string) {
140
+ err := os.MkdirAll("lang", 0777)
141
+ if err != nil {
142
+ utils.Log.Fatal("failed create folder: %s", err.Error())
143
+ }
144
+ generateDriversJson()
145
+ generateSettingsJson()
146
+ },
147
+ }
148
+
149
+ func init() {
150
+ RootCmd.AddCommand(LangCmd)
151
+
152
+ // Here you will define your flags and configuration settings.
153
+
154
+ // Cobra supports Persistent Flags which will work for this command
155
+ // and all subcommands, e.g.:
156
+ // langCmd.PersistentFlags().String("foo", "", "A help for foo")
157
+
158
+ // Cobra supports local flags which will only run when this command
159
+ // is called directly, e.g.:
160
+ // langCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
161
+ }
cmd/restart.go ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "github.com/spf13/cobra"
8
+ )
9
+
10
+ // RestartCmd represents the restart command
11
+ var RestartCmd = &cobra.Command{
12
+ Use: "restart",
13
+ Short: "Restart alist server by daemon/pid file",
14
+ Run: func(cmd *cobra.Command, args []string) {
15
+ stop()
16
+ start()
17
+ },
18
+ }
19
+
20
+ func init() {
21
+ RootCmd.AddCommand(RestartCmd)
22
+
23
+ // Here you will define your flags and configuration settings.
24
+
25
+ // Cobra supports Persistent Flags which will work for this command
26
+ // and all subcommands, e.g.:
27
+ // restartCmd.PersistentFlags().String("foo", "", "A help for foo")
28
+
29
+ // Cobra supports local flags which will only run when this command
30
+ // is called directly, e.g.:
31
+ // restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
32
+ }
cmd/root.go ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+
7
+ "github.com/alist-org/alist/v3/cmd/flags"
8
+ _ "github.com/alist-org/alist/v3/drivers"
9
+ _ "github.com/alist-org/alist/v3/internal/offline_download"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var RootCmd = &cobra.Command{
14
+ Use: "alist",
15
+ Short: "A file list program that supports multiple storage.",
16
+ Long: `A file list program that supports multiple storage,
17
+ built with love by Xhofe and friends in Go/Solid.js.
18
+ Complete documentation is available at https://alist.nn.ci/`,
19
+ }
20
+
21
+ func Execute() {
22
+ if err := RootCmd.Execute(); err != nil {
23
+ fmt.Fprintln(os.Stderr, err)
24
+ os.Exit(1)
25
+ }
26
+ }
27
+
28
+ func init() {
29
+ RootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "data folder")
30
+ RootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
31
+ RootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
32
+ RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
33
+ RootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
34
+ RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", false, "Force to log to std")
35
+ }
cmd/server.go ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "net"
8
+ "net/http"
9
+ "os"
10
+ "os/signal"
11
+ "strconv"
12
+ "sync"
13
+ "syscall"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/cmd/flags"
17
+ "github.com/alist-org/alist/v3/internal/bootstrap"
18
+ "github.com/alist-org/alist/v3/internal/conf"
19
+ "github.com/alist-org/alist/v3/pkg/utils"
20
+ "github.com/alist-org/alist/v3/server"
21
+ "github.com/gin-gonic/gin"
22
+ log "github.com/sirupsen/logrus"
23
+ "github.com/spf13/cobra"
24
+ )
25
+
26
+ // ServerCmd represents the server command
27
+ var ServerCmd = &cobra.Command{
28
+ Use: "server",
29
+ Short: "Start the server at the specified address",
30
+ Long: `Start the server at the specified address
31
+ the address is defined in config file`,
32
+ Run: func(cmd *cobra.Command, args []string) {
33
+ Init()
34
+ if conf.Conf.DelayedStart != 0 {
35
+ utils.Log.Infof("delayed start for %d seconds", conf.Conf.DelayedStart)
36
+ time.Sleep(time.Duration(conf.Conf.DelayedStart) * time.Second)
37
+ }
38
+ bootstrap.InitOfflineDownloadTools()
39
+ bootstrap.LoadStorages()
40
+ bootstrap.InitTaskManager()
41
+ if !flags.Debug && !flags.Dev {
42
+ gin.SetMode(gin.ReleaseMode)
43
+ }
44
+ r := gin.New()
45
+ r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
46
+ server.Init(r)
47
+ var httpSrv, httpsSrv, unixSrv *http.Server
48
+ if conf.Conf.Scheme.HttpPort != -1 {
49
+ httpBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpPort)
50
+ utils.Log.Infof("start HTTP server @ %s", httpBase)
51
+ httpSrv = &http.Server{Addr: httpBase, Handler: r}
52
+ go func() {
53
+ err := httpSrv.ListenAndServe()
54
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
55
+ utils.Log.Fatalf("failed to start http: %s", err.Error())
56
+ }
57
+ }()
58
+ }
59
+ if conf.Conf.Scheme.HttpsPort != -1 {
60
+ httpsBase := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.Scheme.HttpsPort)
61
+ utils.Log.Infof("start HTTPS server @ %s", httpsBase)
62
+ httpsSrv = &http.Server{Addr: httpsBase, Handler: r}
63
+ go func() {
64
+ err := httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
65
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
66
+ utils.Log.Fatalf("failed to start https: %s", err.Error())
67
+ }
68
+ }()
69
+ }
70
+ if conf.Conf.Scheme.UnixFile != "" {
71
+ utils.Log.Infof("start unix server @ %s", conf.Conf.Scheme.UnixFile)
72
+ unixSrv = &http.Server{Handler: r}
73
+ go func() {
74
+ listener, err := net.Listen("unix", conf.Conf.Scheme.UnixFile)
75
+ if err != nil {
76
+ utils.Log.Fatalf("failed to listen unix: %+v", err)
77
+ }
78
+ // set socket file permission
79
+ mode, err := strconv.ParseUint(conf.Conf.Scheme.UnixFilePerm, 8, 32)
80
+ if err != nil {
81
+ utils.Log.Errorf("failed to parse socket file permission: %+v", err)
82
+ } else {
83
+ err = os.Chmod(conf.Conf.Scheme.UnixFile, os.FileMode(mode))
84
+ if err != nil {
85
+ utils.Log.Errorf("failed to chmod socket file: %+v", err)
86
+ }
87
+ }
88
+ err = unixSrv.Serve(listener)
89
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
90
+ utils.Log.Fatalf("failed to start unix: %s", err.Error())
91
+ }
92
+ }()
93
+ }
94
+ if conf.Conf.S3.Port != -1 && conf.Conf.S3.Enable {
95
+ s3r := gin.New()
96
+ s3r.Use(gin.LoggerWithWriter(log.StandardLogger().Out), gin.RecoveryWithWriter(log.StandardLogger().Out))
97
+ server.InitS3(s3r)
98
+ s3Base := fmt.Sprintf("%s:%d", conf.Conf.Scheme.Address, conf.Conf.S3.Port)
99
+ utils.Log.Infof("start S3 server @ %s", s3Base)
100
+ go func() {
101
+ var err error
102
+ if conf.Conf.S3.SSL {
103
+ httpsSrv = &http.Server{Addr: s3Base, Handler: s3r}
104
+ err = httpsSrv.ListenAndServeTLS(conf.Conf.Scheme.CertFile, conf.Conf.Scheme.KeyFile)
105
+ }
106
+ if !conf.Conf.S3.SSL {
107
+ httpSrv = &http.Server{Addr: s3Base, Handler: s3r}
108
+ err = httpSrv.ListenAndServe()
109
+ }
110
+ if err != nil && !errors.Is(err, http.ErrServerClosed) {
111
+ utils.Log.Fatalf("failed to start s3 server: %s", err.Error())
112
+ }
113
+ }()
114
+ }
115
+ // Wait for interrupt signal to gracefully shutdown the server with
116
+ // a timeout of 1 second.
117
+ quit := make(chan os.Signal, 1)
118
+ // kill (no param) default send syscanll.SIGTERM
119
+ // kill -2 is syscall.SIGINT
120
+ // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it
121
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
122
+ <-quit
123
+ utils.Log.Println("Shutdown server...")
124
+ Release()
125
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
126
+ defer cancel()
127
+ var wg sync.WaitGroup
128
+ if conf.Conf.Scheme.HttpPort != -1 {
129
+ wg.Add(1)
130
+ go func() {
131
+ defer wg.Done()
132
+ if err := httpSrv.Shutdown(ctx); err != nil {
133
+ utils.Log.Fatal("HTTP server shutdown err: ", err)
134
+ }
135
+ }()
136
+ }
137
+ if conf.Conf.Scheme.HttpsPort != -1 {
138
+ wg.Add(1)
139
+ go func() {
140
+ defer wg.Done()
141
+ if err := httpsSrv.Shutdown(ctx); err != nil {
142
+ utils.Log.Fatal("HTTPS server shutdown err: ", err)
143
+ }
144
+ }()
145
+ }
146
+ if conf.Conf.Scheme.UnixFile != "" {
147
+ wg.Add(1)
148
+ go func() {
149
+ defer wg.Done()
150
+ if err := unixSrv.Shutdown(ctx); err != nil {
151
+ utils.Log.Fatal("Unix server shutdown err: ", err)
152
+ }
153
+ }()
154
+ }
155
+ wg.Wait()
156
+ utils.Log.Println("Server exit")
157
+ },
158
+ }
159
+
160
+ func init() {
161
+ RootCmd.AddCommand(ServerCmd)
162
+
163
+ // Here you will define your flags and configuration settings.
164
+
165
+ // Cobra supports Persistent Flags which will work for this command
166
+ // and all subcommands, e.g.:
167
+ // serverCmd.PersistentFlags().String("foo", "", "A help for foo")
168
+
169
+ // Cobra supports local flags which will only run when this command
170
+ // is called directly, e.g.:
171
+ // serverCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
172
+ }
173
+
174
+ // OutAlistInit 暴露用于外部启动server的函数
175
+ func OutAlistInit() {
176
+ var (
177
+ cmd *cobra.Command
178
+ args []string
179
+ )
180
+ ServerCmd.Run(cmd, args)
181
+ }
cmd/start.go ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "strconv"
11
+
12
+ log "github.com/sirupsen/logrus"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ // StartCmd represents the start command
17
+ var StartCmd = &cobra.Command{
18
+ Use: "start",
19
+ Short: "Silent start alist server with `--force-bin-dir`",
20
+ Run: func(cmd *cobra.Command, args []string) {
21
+ start()
22
+ },
23
+ }
24
+
25
+ func start() {
26
+ initDaemon()
27
+ if pid != -1 {
28
+ _, err := os.FindProcess(pid)
29
+ if err == nil {
30
+ log.Info("alist already started, pid ", pid)
31
+ return
32
+ }
33
+ }
34
+ args := os.Args
35
+ args[1] = "server"
36
+ args = append(args, "--force-bin-dir")
37
+ cmd := &exec.Cmd{
38
+ Path: args[0],
39
+ Args: args,
40
+ Env: os.Environ(),
41
+ }
42
+ stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
43
+ if err != nil {
44
+ log.Fatal(os.Getpid(), ": failed to open start log file:", err)
45
+ }
46
+ cmd.Stderr = stdout
47
+ cmd.Stdout = stdout
48
+ err = cmd.Start()
49
+ if err != nil {
50
+ log.Fatal("failed to start children process: ", err)
51
+ }
52
+ log.Infof("success start pid: %d", cmd.Process.Pid)
53
+ err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
54
+ if err != nil {
55
+ log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`")
56
+ }
57
+ }
58
+
59
+ func init() {
60
+ RootCmd.AddCommand(StartCmd)
61
+
62
+ // Here you will define your flags and configuration settings.
63
+
64
+ // Cobra supports Persistent Flags which will work for this command
65
+ // and all subcommands, e.g.:
66
+ // startCmd.PersistentFlags().String("foo", "", "A help for foo")
67
+
68
+ // Cobra supports local flags which will only run when this command
69
+ // is called directly, e.g.:
70
+ // startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
71
+ }
cmd/stop.go ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+
9
+ log "github.com/sirupsen/logrus"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ // StopCmd represents the stop command
14
+ var StopCmd = &cobra.Command{
15
+ Use: "stop",
16
+ Short: "Stop alist server by daemon/pid file",
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ stop()
19
+ },
20
+ }
21
+
22
+ func stop() {
23
+ initDaemon()
24
+ if pid == -1 {
25
+ log.Info("Seems not have been started. Try use `alist start` to start server.")
26
+ return
27
+ }
28
+ process, err := os.FindProcess(pid)
29
+ if err != nil {
30
+ log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
31
+ return
32
+ }
33
+ err = process.Kill()
34
+ if err != nil {
35
+ log.Errorf("failed to kill process %d: %v", pid, err)
36
+ } else {
37
+ log.Info("killed process: ", pid)
38
+ }
39
+ err = os.Remove(pidFile)
40
+ if err != nil {
41
+ log.Errorf("failed to remove pid file")
42
+ }
43
+ pid = -1
44
+ }
45
+
46
+ func init() {
47
+ RootCmd.AddCommand(StopCmd)
48
+
49
+ // Here you will define your flags and configuration settings.
50
+
51
+ // Cobra supports Persistent Flags which will work for this command
52
+ // and all subcommands, e.g.:
53
+ // stopCmd.PersistentFlags().String("foo", "", "A help for foo")
54
+
55
+ // Cobra supports local flags which will only run when this command
56
+ // is called directly, e.g.:
57
+ // stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
58
+ }
cmd/storage.go ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2023 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "os"
8
+ "strconv"
9
+
10
+ "github.com/alist-org/alist/v3/internal/db"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/charmbracelet/bubbles/table"
13
+ tea "github.com/charmbracelet/bubbletea"
14
+ "github.com/charmbracelet/lipgloss"
15
+ "github.com/spf13/cobra"
16
+ )
17
+
18
+ // storageCmd represents the storage command
19
+ var storageCmd = &cobra.Command{
20
+ Use: "storage",
21
+ Short: "Manage storage",
22
+ }
23
+
24
+ var disableStorageCmd = &cobra.Command{
25
+ Use: "disable",
26
+ Short: "Disable a storage",
27
+ Run: func(cmd *cobra.Command, args []string) {
28
+ if len(args) < 1 {
29
+ utils.Log.Errorf("mount path is required")
30
+ return
31
+ }
32
+ mountPath := args[0]
33
+ Init()
34
+ defer Release()
35
+ storage, err := db.GetStorageByMountPath(mountPath)
36
+ if err != nil {
37
+ utils.Log.Errorf("failed to query storage: %+v", err)
38
+ } else {
39
+ storage.Disabled = true
40
+ err = db.UpdateStorage(storage)
41
+ if err != nil {
42
+ utils.Log.Errorf("failed to update storage: %+v", err)
43
+ } else {
44
+ utils.Log.Infof("Storage with mount path [%s] have been disabled", mountPath)
45
+ }
46
+ }
47
+ },
48
+ }
49
+
50
+ var baseStyle = lipgloss.NewStyle().
51
+ BorderStyle(lipgloss.NormalBorder()).
52
+ BorderForeground(lipgloss.Color("240"))
53
+
54
+ type model struct {
55
+ table table.Model
56
+ }
57
+
58
+ func (m model) Init() tea.Cmd { return nil }
59
+
60
+ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
61
+ var cmd tea.Cmd
62
+ switch msg := msg.(type) {
63
+ case tea.KeyMsg:
64
+ switch msg.String() {
65
+ case "esc":
66
+ if m.table.Focused() {
67
+ m.table.Blur()
68
+ } else {
69
+ m.table.Focus()
70
+ }
71
+ case "q", "ctrl+c":
72
+ return m, tea.Quit
73
+ //case "enter":
74
+ // return m, tea.Batch(
75
+ // tea.Printf("Let's go to %s!", m.table.SelectedRow()[1]),
76
+ // )
77
+ }
78
+ }
79
+ m.table, cmd = m.table.Update(msg)
80
+ return m, cmd
81
+ }
82
+
83
+ func (m model) View() string {
84
+ return baseStyle.Render(m.table.View()) + "\n"
85
+ }
86
+
87
+ var storageTableHeight int
88
+ var listStorageCmd = &cobra.Command{
89
+ Use: "list",
90
+ Short: "List all storages",
91
+ Run: func(cmd *cobra.Command, args []string) {
92
+ Init()
93
+ defer Release()
94
+ storages, _, err := db.GetStorages(1, -1)
95
+ if err != nil {
96
+ utils.Log.Errorf("failed to query storages: %+v", err)
97
+ } else {
98
+ utils.Log.Infof("Found %d storages", len(storages))
99
+ columns := []table.Column{
100
+ {Title: "ID", Width: 4},
101
+ {Title: "Driver", Width: 16},
102
+ {Title: "Mount Path", Width: 30},
103
+ {Title: "Enabled", Width: 7},
104
+ }
105
+
106
+ var rows []table.Row
107
+ for i := range storages {
108
+ storage := storages[i]
109
+ enabled := "true"
110
+ if storage.Disabled {
111
+ enabled = "false"
112
+ }
113
+ rows = append(rows, table.Row{
114
+ strconv.Itoa(int(storage.ID)),
115
+ storage.Driver,
116
+ storage.MountPath,
117
+ enabled,
118
+ })
119
+ }
120
+ t := table.New(
121
+ table.WithColumns(columns),
122
+ table.WithRows(rows),
123
+ table.WithFocused(true),
124
+ table.WithHeight(storageTableHeight),
125
+ )
126
+
127
+ s := table.DefaultStyles()
128
+ s.Header = s.Header.
129
+ BorderStyle(lipgloss.NormalBorder()).
130
+ BorderForeground(lipgloss.Color("240")).
131
+ BorderBottom(true).
132
+ Bold(false)
133
+ s.Selected = s.Selected.
134
+ Foreground(lipgloss.Color("229")).
135
+ Background(lipgloss.Color("57")).
136
+ Bold(false)
137
+ t.SetStyles(s)
138
+
139
+ m := model{t}
140
+ if _, err := tea.NewProgram(m).Run(); err != nil {
141
+ utils.Log.Errorf("failed to run program: %+v", err)
142
+ os.Exit(1)
143
+ }
144
+ }
145
+ },
146
+ }
147
+
148
+ func init() {
149
+
150
+ RootCmd.AddCommand(storageCmd)
151
+ storageCmd.AddCommand(disableStorageCmd)
152
+ storageCmd.AddCommand(listStorageCmd)
153
+ storageCmd.PersistentFlags().IntVarP(&storageTableHeight, "height", "H", 10, "Table height")
154
+ // Here you will define your flags and configuration settings.
155
+
156
+ // Cobra supports Persistent Flags which will work for this command
157
+ // and all subcommands, e.g.:
158
+ // storageCmd.PersistentFlags().String("foo", "", "A help for foo")
159
+
160
+ // Cobra supports local flags which will only run when this command
161
+ // is called directly, e.g.:
162
+ // storageCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
163
+ }
cmd/user.go ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package cmd
2
+
3
+ import (
4
+ "crypto/tls"
5
+ "fmt"
6
+ "time"
7
+
8
+ "github.com/alist-org/alist/v3/internal/conf"
9
+ "github.com/alist-org/alist/v3/internal/op"
10
+ "github.com/alist-org/alist/v3/internal/setting"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/go-resty/resty/v2"
13
+ )
14
+
15
+ func DelAdminCacheOnline() {
16
+ admin, err := op.GetAdmin()
17
+ if err != nil {
18
+ utils.Log.Errorf("[del_admin_cache] get admin error: %+v", err)
19
+ return
20
+ }
21
+ DelUserCacheOnline(admin.Username)
22
+ }
23
+
24
+ func DelUserCacheOnline(username string) {
25
+ client := resty.New().SetTimeout(1 * time.Second).SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
26
+ token := setting.GetStr(conf.Token)
27
+ port := conf.Conf.Scheme.HttpPort
28
+ u := fmt.Sprintf("http://localhost:%d/api/admin/user/del_cache", port)
29
+ if port == -1 {
30
+ if conf.Conf.Scheme.HttpsPort == -1 {
31
+ utils.Log.Warnf("[del_user_cache] no open port")
32
+ return
33
+ }
34
+ u = fmt.Sprintf("https://localhost:%d/api/admin/user/del_cache", conf.Conf.Scheme.HttpsPort)
35
+ }
36
+ res, err := client.R().SetHeader("Authorization", token).SetQueryParam("username", username).Post(u)
37
+ if err != nil {
38
+ utils.Log.Warnf("[del_user_cache_online] failed: %+v", err)
39
+ return
40
+ }
41
+ if res.StatusCode() != 200 {
42
+ utils.Log.Warnf("[del_user_cache_online] failed: %+v", res.String())
43
+ return
44
+ }
45
+ code := utils.Json.Get(res.Body(), "code").ToInt()
46
+ msg := utils.Json.Get(res.Body(), "message").ToString()
47
+ if code != 200 {
48
+ utils.Log.Errorf("[del_user_cache_online] error: %s", msg)
49
+ return
50
+ }
51
+ utils.Log.Debugf("[del_user_cache_online] del user [%s] cache success", username)
52
+ }
cmd/version.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ Copyright © 2022 NAME HERE <EMAIL ADDRESS>
3
+ */
4
+ package cmd
5
+
6
+ import (
7
+ "fmt"
8
+ "os"
9
+
10
+ "github.com/alist-org/alist/v3/internal/conf"
11
+ "github.com/spf13/cobra"
12
+ )
13
+
14
+ // VersionCmd represents the version command
15
+ var VersionCmd = &cobra.Command{
16
+ Use: "version",
17
+ Short: "Show current version of AList",
18
+ Run: func(cmd *cobra.Command, args []string) {
19
+ fmt.Printf(`Built At: %s
20
+ Go Version: %s
21
+ Author: %s
22
+ Commit ID: %s
23
+ Version: %s
24
+ WebVersion: %s
25
+ `,
26
+ conf.BuiltAt, conf.GoVersion, conf.GitAuthor, conf.GitCommit, conf.Version, conf.WebVersion)
27
+ os.Exit(0)
28
+ },
29
+ }
30
+
31
+ func init() {
32
+ RootCmd.AddCommand(VersionCmd)
33
+
34
+ // Here you will define your flags and configuration settings.
35
+
36
+ // Cobra supports Persistent Flags which will work for this command
37
+ // and all subcommands, e.g.:
38
+ // versionCmd.PersistentFlags().String("foo", "", "A help for foo")
39
+
40
+ // Cobra supports local flags which will only run when this command
41
+ // is called directly, e.g.:
42
+ // versionCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
43
+ }
data/config.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "force": true,
3
+ "site_url": "",
4
+ "cdn": "",
5
+ "jwt_secret": "random_generated",
6
+ "token_expires_in": 48,
7
+ "database": {
8
+ "type": "sqlite3",
9
+ "host": "",
10
+ "port": 0,
11
+ "user": "",
12
+ "password": "",
13
+ "name": "",
14
+ "db_file": "/home/user/data/data.db",
15
+ "table_prefix": "x_",
16
+ "ssl_mode": "",
17
+ "dsn": ""
18
+ }
19
+ }
docker-compose.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.3'
2
+ services:
3
+ alist:
4
+ restart: always
5
+ volumes:
6
+ - '/etc/alist:/opt/alist/data'
7
+ ports:
8
+ - '5244:5244'
9
+ - '5245:5245'
10
+ environment:
11
+ - PUID=0
12
+ - PGID=0
13
+ - UMASK=022
14
+ - TZ=UTC
15
+ container_name: alist
16
+ image: 'xhofe/alist:latest'
drivers/115/appver.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
5
+ "github.com/alist-org/alist/v3/drivers/base"
6
+ log "github.com/sirupsen/logrus"
7
+ )
8
+
9
+ var (
10
+ md5Salt = "Qclm8MGWUv59TnrR0XPg"
11
+ appVer = "27.0.5.7"
12
+ )
13
+
14
+ func (d *Pan115) getAppVersion() ([]driver115.AppVersion, error) {
15
+ result := driver115.VersionResp{}
16
+ resp, err := base.RestyClient.R().Get(driver115.ApiGetVersion)
17
+
18
+ err = driver115.CheckErr(err, &result, resp)
19
+ if err != nil {
20
+ return nil, err
21
+ }
22
+
23
+ return result.Data.GetAppVersions(), nil
24
+ }
25
+
26
+ func (d *Pan115) getAppVer() string {
27
+ // todo add some cache?
28
+ vers, err := d.getAppVersion()
29
+ if err != nil {
30
+ log.Warnf("[115] get app version failed: %v", err)
31
+ return appVer
32
+ }
33
+ for _, ver := range vers {
34
+ if ver.AppName == "win" {
35
+ return ver.Version
36
+ }
37
+ }
38
+ return appVer
39
+ }
40
+
41
+ func (d *Pan115) initAppVer() {
42
+ appVer = d.getAppVer()
43
+ }
drivers/115/driver.go ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+ "sync"
7
+
8
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
9
+ "github.com/alist-org/alist/v3/internal/driver"
10
+ "github.com/alist-org/alist/v3/internal/model"
11
+ "github.com/alist-org/alist/v3/pkg/http_range"
12
+ "github.com/alist-org/alist/v3/pkg/utils"
13
+ "github.com/pkg/errors"
14
+ "golang.org/x/time/rate"
15
+ )
16
+
17
+ type Pan115 struct {
18
+ model.Storage
19
+ Addition
20
+ client *driver115.Pan115Client
21
+ limiter *rate.Limiter
22
+ appVerOnce sync.Once
23
+ }
24
+
25
+ func (d *Pan115) Config() driver.Config {
26
+ return config
27
+ }
28
+
29
+ func (d *Pan115) GetAddition() driver.Additional {
30
+ return &d.Addition
31
+ }
32
+
33
+ func (d *Pan115) Init(ctx context.Context) error {
34
+ d.appVerOnce.Do(d.initAppVer)
35
+ if d.LimitRate > 0 {
36
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
37
+ }
38
+ return d.login()
39
+ }
40
+
41
+ func (d *Pan115) WaitLimit(ctx context.Context) error {
42
+ if d.limiter != nil {
43
+ return d.limiter.Wait(ctx)
44
+ }
45
+ return nil
46
+ }
47
+
48
+ func (d *Pan115) Drop(ctx context.Context) error {
49
+ return nil
50
+ }
51
+
52
+ func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
53
+ if err := d.WaitLimit(ctx); err != nil {
54
+ return nil, err
55
+ }
56
+ files, err := d.getFiles(dir.GetID())
57
+ if err != nil && !errors.Is(err, driver115.ErrNotExist) {
58
+ return nil, err
59
+ }
60
+ return utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {
61
+ return &src, nil
62
+ })
63
+ }
64
+
65
+ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
66
+ if err := d.WaitLimit(ctx); err != nil {
67
+ return nil, err
68
+ }
69
+ userAgent := args.Header.Get("User-Agent")
70
+ downloadInfo, err := d.
71
+ DownloadWithUA(file.(*FileObj).PickCode, userAgent)
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ link := &model.Link{
76
+ URL: downloadInfo.Url.Url,
77
+ Header: downloadInfo.Header,
78
+ }
79
+ return link, nil
80
+ }
81
+
82
+ func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) (model.Obj, error) {
83
+ if err := d.WaitLimit(ctx); err != nil {
84
+ return nil, err
85
+ }
86
+
87
+ result := driver115.MkdirResp{}
88
+ form := map[string]string{
89
+ "pid": parentDir.GetID(),
90
+ "cname": dirName,
91
+ }
92
+ req := d.client.NewRequest().
93
+ SetFormData(form).
94
+ SetResult(&result).
95
+ ForceContentType("application/json;charset=UTF-8")
96
+
97
+ resp, err := req.Post(driver115.ApiDirAdd)
98
+
99
+ err = driver115.CheckErr(err, &result, resp)
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+ f, err := d.getNewFile(result.FileID)
104
+ if err != nil {
105
+ return nil, nil
106
+ }
107
+ return f, nil
108
+ }
109
+
110
+ func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
111
+ if err := d.WaitLimit(ctx); err != nil {
112
+ return nil, err
113
+ }
114
+ if err := d.client.Move(dstDir.GetID(), srcObj.GetID()); err != nil {
115
+ return nil, err
116
+ }
117
+ f, err := d.getNewFile(srcObj.GetID())
118
+ if err != nil {
119
+ return nil, nil
120
+ }
121
+ return f, nil
122
+ }
123
+
124
+ func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) (model.Obj, error) {
125
+ if err := d.WaitLimit(ctx); err != nil {
126
+ return nil, err
127
+ }
128
+ if err := d.client.Rename(srcObj.GetID(), newName); err != nil {
129
+ return nil, err
130
+ }
131
+ f, err := d.getNewFile((srcObj.GetID()))
132
+ if err != nil {
133
+ return nil, nil
134
+ }
135
+ return f, nil
136
+ }
137
+
138
+ func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
139
+ if err := d.WaitLimit(ctx); err != nil {
140
+ return err
141
+ }
142
+ return d.client.Copy(dstDir.GetID(), srcObj.GetID())
143
+ }
144
+
145
+ func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
146
+ if err := d.WaitLimit(ctx); err != nil {
147
+ return err
148
+ }
149
+ return d.client.Delete(obj.GetID())
150
+ }
151
+
152
+ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) (model.Obj, error) {
153
+ if err := d.WaitLimit(ctx); err != nil {
154
+ return nil, err
155
+ }
156
+
157
+ var (
158
+ fastInfo *driver115.UploadInitResp
159
+ dirID = dstDir.GetID()
160
+ )
161
+
162
+ if ok, err := d.client.UploadAvailable(); err != nil || !ok {
163
+ return nil, err
164
+ }
165
+ if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
166
+ return nil, driver115.ErrUploadTooLarge
167
+ }
168
+ //if digest, err = d.client.GetDigestResult(stream); err != nil {
169
+ // return err
170
+ //}
171
+
172
+ const PreHashSize int64 = 128 * utils.KB
173
+ hashSize := PreHashSize
174
+ if stream.GetSize() < PreHashSize {
175
+ hashSize = stream.GetSize()
176
+ }
177
+ reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
178
+ if err != nil {
179
+ return nil, err
180
+ }
181
+ preHash, err := utils.HashReader(utils.SHA1, reader)
182
+ if err != nil {
183
+ return nil, err
184
+ }
185
+ preHash = strings.ToUpper(preHash)
186
+ fullHash := stream.GetHash().GetHash(utils.SHA1)
187
+ if len(fullHash) <= 0 {
188
+ tmpF, err := stream.CacheFullInTempFile()
189
+ if err != nil {
190
+ return nil, err
191
+ }
192
+ fullHash, err = utils.HashFile(utils.SHA1, tmpF)
193
+ if err != nil {
194
+ return nil, err
195
+ }
196
+ }
197
+ fullHash = strings.ToUpper(fullHash)
198
+
199
+ // rapid-upload
200
+ // note that 115 add timeout for rapid-upload,
201
+ // and "sig invalid" err is thrown even when the hash is correct after timeout.
202
+ if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
203
+ return nil, err
204
+ }
205
+ if matched, err := fastInfo.Ok(); err != nil {
206
+ return nil, err
207
+ } else if matched {
208
+ f, err := d.getNewFileByPickCode(fastInfo.PickCode)
209
+ if err != nil {
210
+ return nil, nil
211
+ }
212
+ return f, nil
213
+ }
214
+
215
+ var uploadResult *UploadResult
216
+ // 闪传失败,上传
217
+ if stream.GetSize() <= 10*utils.MB { // 文件大小小于10MB,改用普通模式上传
218
+ if uploadResult, err = d.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID); err != nil {
219
+ return nil, err
220
+ }
221
+ } else {
222
+ // 分片上传
223
+ if uploadResult, err = d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID); err != nil {
224
+ return nil, err
225
+ }
226
+ }
227
+
228
+ file, err := d.getNewFile(uploadResult.Data.FileID)
229
+ if err != nil {
230
+ return nil, nil
231
+ }
232
+ return file, nil
233
+ }
234
+
235
+ func (d *Pan115) OfflineList(ctx context.Context) ([]*driver115.OfflineTask, error) {
236
+ resp, err := d.client.ListOfflineTask(0)
237
+ if err != nil {
238
+ return nil, err
239
+ }
240
+ return resp.Tasks, nil
241
+ }
242
+
243
+ func (d *Pan115) OfflineDownload(ctx context.Context, uris []string, dstDir model.Obj) ([]string, error) {
244
+ return d.client.AddOfflineTaskURIs(uris, dstDir.GetID())
245
+ }
246
+
247
+ func (d *Pan115) DeleteOfflineTasks(ctx context.Context, hashes []string, deleteFiles bool) error {
248
+ return d.client.DeleteOfflineTasks(hashes, deleteFiles)
249
+ }
250
+
251
+ var _ driver.Driver = (*Pan115)(nil)
drivers/115/meta.go ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
10
+ QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
11
+ QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate ([limit]r/1s)"`
14
+ driver.RootID
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "115 Cloud",
19
+ DefaultRoot: "0",
20
+ // OnlyProxy: true,
21
+ // OnlyLocal: true,
22
+ // NoOverwriteUpload: true,
23
+ }
24
+
25
+ func init() {
26
+ op.RegisterDriver(func() driver.Driver {
27
+ return &Pan115{}
28
+ })
29
+ }
drivers/115/types.go ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "time"
5
+
6
+ "github.com/SheltonZhu/115driver/pkg/driver"
7
+ "github.com/alist-org/alist/v3/internal/model"
8
+ "github.com/alist-org/alist/v3/pkg/utils"
9
+ )
10
+
11
+ var _ model.Obj = (*FileObj)(nil)
12
+
13
+ type FileObj struct {
14
+ driver.File
15
+ }
16
+
17
+ func (f *FileObj) CreateTime() time.Time {
18
+ return f.File.CreateTime
19
+ }
20
+
21
+ func (f *FileObj) GetHash() utils.HashInfo {
22
+ return utils.NewHashInfo(utils.SHA1, f.Sha1)
23
+ }
24
+
25
+ type UploadResult struct {
26
+ driver.BasicResp
27
+ Data struct {
28
+ PickCode string `json:"pick_code"`
29
+ FileSize int `json:"file_size"`
30
+ FileID string `json:"file_id"`
31
+ ThumbURL string `json:"thumb_url"`
32
+ Sha1 string `json:"sha1"`
33
+ Aid int `json:"aid"`
34
+ FileName string `json:"file_name"`
35
+ Cid string `json:"cid"`
36
+ IsVideo int `json:"is_video"`
37
+ } `json:"data"`
38
+ }
drivers/115/util.go ADDED
@@ -0,0 +1,537 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/md5"
6
+ "crypto/tls"
7
+ "encoding/hex"
8
+ "encoding/json"
9
+ "fmt"
10
+ "io"
11
+ "net/http"
12
+ "net/url"
13
+ "strconv"
14
+ "strings"
15
+ "sync"
16
+ "time"
17
+
18
+ "github.com/alist-org/alist/v3/internal/conf"
19
+ "github.com/alist-org/alist/v3/internal/model"
20
+ "github.com/alist-org/alist/v3/pkg/http_range"
21
+ "github.com/alist-org/alist/v3/pkg/utils"
22
+ "github.com/aliyun/aliyun-oss-go-sdk/oss"
23
+
24
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
25
+ crypto "github.com/gaoyb7/115drive-webdav/115"
26
+ "github.com/orzogc/fake115uploader/cipher"
27
+ "github.com/pkg/errors"
28
+ )
29
+
30
+ // var UserAgent = driver115.UA115Browser
31
+ func (d *Pan115) login() error {
32
+ var err error
33
+ opts := []driver115.Option{
34
+ driver115.UA(d.getUA()),
35
+ func(c *driver115.Pan115Client) {
36
+ c.Client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: conf.Conf.TlsInsecureSkipVerify})
37
+ },
38
+ }
39
+ d.client = driver115.New(opts...)
40
+ cr := &driver115.Credential{}
41
+ if d.QRCodeToken != "" {
42
+ s := &driver115.QRCodeSession{
43
+ UID: d.QRCodeToken,
44
+ }
45
+ if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
46
+ return errors.Wrap(err, "failed to login by qrcode")
47
+ }
48
+ d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
49
+ d.QRCodeToken = ""
50
+ } else if d.Cookie != "" {
51
+ if err = cr.FromCookie(d.Cookie); err != nil {
52
+ return errors.Wrap(err, "failed to login by cookies")
53
+ }
54
+ d.client.ImportCredential(cr)
55
+ } else {
56
+ return errors.New("missing cookie or qrcode account")
57
+ }
58
+ return d.client.LoginCheck()
59
+ }
60
+
61
+ func (d *Pan115) getFiles(fileId string) ([]FileObj, error) {
62
+ res := make([]FileObj, 0)
63
+ if d.PageSize <= 0 {
64
+ d.PageSize = driver115.FileListLimit
65
+ }
66
+ files, err := d.client.ListWithLimit(fileId, d.PageSize)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ for _, file := range *files {
71
+ res = append(res, FileObj{file})
72
+ }
73
+ return res, nil
74
+ }
75
+
76
+ func (d *Pan115) getNewFile(fileId string) (*FileObj, error) {
77
+ file, err := d.client.GetFile(fileId)
78
+ if err != nil {
79
+ return nil, err
80
+ }
81
+ return &FileObj{*file}, nil
82
+ }
83
+
84
+ func (d *Pan115) getNewFileByPickCode(pickCode string) (*FileObj, error) {
85
+ result := driver115.GetFileInfoResponse{}
86
+ req := d.client.NewRequest().
87
+ SetQueryParam("pick_code", pickCode).
88
+ ForceContentType("application/json;charset=UTF-8").
89
+ SetResult(&result)
90
+ resp, err := req.Get(driver115.ApiFileInfo)
91
+ if err := driver115.CheckErr(err, &result, resp); err != nil {
92
+ return nil, err
93
+ }
94
+ if len(result.Files) == 0 {
95
+ return nil, errors.New("not get file info")
96
+ }
97
+ fileInfo := result.Files[0]
98
+
99
+ f := &FileObj{}
100
+ f.From(fileInfo)
101
+ return f, nil
102
+ }
103
+
104
+ func (d *Pan115) getUA() string {
105
+ return fmt.Sprintf("Mozilla/5.0 115Browser/%s", appVer)
106
+ }
107
+
108
+ func (d *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
109
+ key := crypto.GenerateKey()
110
+ result := driver115.DownloadResp{}
111
+ params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
112
+ if err != nil {
113
+ return nil, err
114
+ }
115
+
116
+ data := crypto.Encode(params, key)
117
+
118
+ bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
119
+ reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
120
+ req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
121
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
122
+ req.Header.Set("Cookie", d.Cookie)
123
+ req.Header.Set("User-Agent", ua)
124
+
125
+ resp, err := d.client.Client.GetClient().Do(req)
126
+ if err != nil {
127
+ return nil, err
128
+ }
129
+ defer resp.Body.Close()
130
+
131
+ body, err := io.ReadAll(resp.Body)
132
+ if err != nil {
133
+ return nil, err
134
+ }
135
+ if err := utils.Json.Unmarshal(body, &result); err != nil {
136
+ return nil, err
137
+ }
138
+
139
+ if err = result.Err(string(body)); err != nil {
140
+ return nil, err
141
+ }
142
+
143
+ bytes, err := crypto.Decode(string(result.EncodedData), key)
144
+ if err != nil {
145
+ return nil, err
146
+ }
147
+
148
+ downloadInfo := driver115.DownloadData{}
149
+ if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil {
150
+ return nil, err
151
+ }
152
+
153
+ for _, info := range downloadInfo {
154
+ if info.FileSize < 0 {
155
+ return nil, driver115.ErrDownloadEmpty
156
+ }
157
+ info.Header = resp.Request.Header
158
+ return info, nil
159
+ }
160
+ return nil, driver115.ErrUnexpected
161
+ }
162
+
163
+ func (c *Pan115) GenerateToken(fileID, preID, timeStamp, fileSize, signKey, signVal string) string {
164
+ userID := strconv.FormatInt(c.client.UserID, 10)
165
+ userIDMd5 := md5.Sum([]byte(userID))
166
+ tokenMd5 := md5.Sum([]byte(md5Salt + fileID + fileSize + signKey + signVal + userID + timeStamp + hex.EncodeToString(userIDMd5[:]) + appVer))
167
+ return hex.EncodeToString(tokenMd5[:])
168
+ }
169
+
170
+ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
171
+ var (
172
+ ecdhCipher *cipher.EcdhCipher
173
+ encrypted []byte
174
+ decrypted []byte
175
+ encodedToken string
176
+ err error
177
+ target = "U_1_" + dirID
178
+ bodyBytes []byte
179
+ result = driver115.UploadInitResp{}
180
+ fileSizeStr = strconv.FormatInt(fileSize, 10)
181
+ )
182
+ if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
183
+ return nil, err
184
+ }
185
+
186
+ userID := strconv.FormatInt(d.client.UserID, 10)
187
+ form := url.Values{}
188
+ form.Set("appid", "0")
189
+ form.Set("appversion", appVer)
190
+ form.Set("userid", userID)
191
+ form.Set("filename", fileName)
192
+ form.Set("filesize", fileSizeStr)
193
+ form.Set("fileid", fileID)
194
+ form.Set("target", target)
195
+ form.Set("sig", d.client.GenerateSignature(fileID, target))
196
+
197
+ signKey, signVal := "", ""
198
+ for retry := true; retry; {
199
+ t := driver115.NowMilli()
200
+
201
+ if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
202
+ return nil, err
203
+ }
204
+
205
+ params := map[string]string{
206
+ "k_ec": encodedToken,
207
+ }
208
+
209
+ form.Set("t", t.String())
210
+ form.Set("token", d.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
211
+ if signKey != "" && signVal != "" {
212
+ form.Set("sign_key", signKey)
213
+ form.Set("sign_val", signVal)
214
+ }
215
+ if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
216
+ return nil, err
217
+ }
218
+
219
+ req := d.client.NewRequest().
220
+ SetQueryParams(params).
221
+ SetBody(encrypted).
222
+ SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
223
+ SetDoNotParseResponse(true)
224
+ resp, err := req.Post(driver115.ApiUploadInit)
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ data := resp.RawBody()
229
+ defer data.Close()
230
+ if bodyBytes, err = io.ReadAll(data); err != nil {
231
+ return nil, err
232
+ }
233
+ if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
234
+ return nil, err
235
+ }
236
+ if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
237
+ return nil, err
238
+ }
239
+ if result.Status == 7 {
240
+ // Update signKey & signVal
241
+ signKey = result.SignKey
242
+ signVal, err = UploadDigestRange(stream, result.SignCheck)
243
+ if err != nil {
244
+ return nil, err
245
+ }
246
+ } else {
247
+ retry = false
248
+ }
249
+ result.SHA1 = fileID
250
+ }
251
+
252
+ return &result, nil
253
+ }
254
+
255
+ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
256
+ var start, end int64
257
+ if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
258
+ return
259
+ }
260
+
261
+ length := end - start + 1
262
+ reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
263
+ if err != nil {
264
+ return "", err
265
+ }
266
+ hashStr, err := utils.HashReader(utils.SHA1, reader)
267
+ if err != nil {
268
+ return "", err
269
+ }
270
+ result = strings.ToUpper(hashStr)
271
+ return
272
+ }
273
+
274
+ // UploadByOSS use aliyun sdk to upload
275
+ func (c *Pan115) UploadByOSS(params *driver115.UploadOSSParams, r io.Reader, dirID string) (*UploadResult, error) {
276
+ ossToken, err := c.client.GetOSSToken()
277
+ if err != nil {
278
+ return nil, err
279
+ }
280
+ ossClient, err := oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret)
281
+ if err != nil {
282
+ return nil, err
283
+ }
284
+ bucket, err := ossClient.Bucket(params.Bucket)
285
+ if err != nil {
286
+ return nil, err
287
+ }
288
+
289
+ var bodyBytes []byte
290
+ if err = bucket.PutObject(params.Object, r, append(
291
+ driver115.OssOption(params, ossToken),
292
+ oss.CallbackResult(&bodyBytes),
293
+ )...); err != nil {
294
+ return nil, err
295
+ }
296
+
297
+ var uploadResult UploadResult
298
+ if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
299
+ return nil, err
300
+ }
301
+ return &uploadResult, uploadResult.Err(string(bodyBytes))
302
+ }
303
+
304
+ // UploadByMultipart upload by mutipart blocks
305
+ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) (*UploadResult, error) {
306
+ var (
307
+ chunks []oss.FileChunk
308
+ parts []oss.UploadPart
309
+ imur oss.InitiateMultipartUploadResult
310
+ ossClient *oss.Client
311
+ bucket *oss.Bucket
312
+ ossToken *driver115.UploadOSSTokenResp
313
+ bodyBytes []byte
314
+ err error
315
+ )
316
+
317
+ tmpF, err := stream.CacheFullInTempFile()
318
+ if err != nil {
319
+ return nil, err
320
+ }
321
+
322
+ options := driver115.DefalutUploadMultipartOptions()
323
+ if len(opts) > 0 {
324
+ for _, f := range opts {
325
+ f(options)
326
+ }
327
+ }
328
+ // oss 启用Sequential必须按顺序上传
329
+ options.ThreadsNum = 1
330
+
331
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
332
+ return nil, err
333
+ }
334
+
335
+ if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret, oss.EnableMD5(true), oss.EnableCRC(true)); err != nil {
336
+ return nil, err
337
+ }
338
+
339
+ if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
340
+ return nil, err
341
+ }
342
+
343
+ // ossToken一小时后就会失效,所以每50分钟重新获取一次
344
+ ticker := time.NewTicker(options.TokenRefreshTime)
345
+ defer ticker.Stop()
346
+ // 设置超时
347
+ timeout := time.NewTimer(options.Timeout)
348
+
349
+ if chunks, err = SplitFile(fileSize); err != nil {
350
+ return nil, err
351
+ }
352
+
353
+ if imur, err = bucket.InitiateMultipartUpload(params.Object,
354
+ oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
355
+ oss.UserAgentHeader(driver115.OSSUserAgent),
356
+ oss.EnableSha1(), oss.Sequential(),
357
+ ); err != nil {
358
+ return nil, err
359
+ }
360
+
361
+ wg := sync.WaitGroup{}
362
+ wg.Add(len(chunks))
363
+
364
+ chunksCh := make(chan oss.FileChunk)
365
+ errCh := make(chan error)
366
+ UploadedPartsCh := make(chan oss.UploadPart)
367
+ quit := make(chan struct{})
368
+
369
+ // producer
370
+ go chunksProducer(chunksCh, chunks)
371
+ go func() {
372
+ wg.Wait()
373
+ quit <- struct{}{}
374
+ }()
375
+
376
+ // consumers
377
+ for i := 0; i < options.ThreadsNum; i++ {
378
+ go func(threadId int) {
379
+ defer func() {
380
+ if r := recover(); r != nil {
381
+ errCh <- fmt.Errorf("recovered in %v", r)
382
+ }
383
+ }()
384
+ for chunk := range chunksCh {
385
+ var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
386
+ for retry := 0; retry < 3; retry++ {
387
+ select {
388
+ case <-ticker.C:
389
+ if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
390
+ errCh <- errors.Wrap(err, "刷新token时出现错误")
391
+ }
392
+ default:
393
+ }
394
+
395
+ buf := make([]byte, chunk.Size)
396
+ if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
397
+ continue
398
+ }
399
+
400
+ if part, err = bucket.UploadPart(imur, bytes.NewBuffer(buf), chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
401
+ break
402
+ }
403
+ }
404
+ if err != nil {
405
+ errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", stream.GetName(), chunk.Number, err))
406
+ }
407
+ UploadedPartsCh <- part
408
+ }
409
+ }(i)
410
+ }
411
+
412
+ go func() {
413
+ for part := range UploadedPartsCh {
414
+ parts = append(parts, part)
415
+ wg.Done()
416
+ }
417
+ }()
418
+ LOOP:
419
+ for {
420
+ select {
421
+ case <-ticker.C:
422
+ // 到时重新获取ossToken
423
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
424
+ return nil, err
425
+ }
426
+ case <-quit:
427
+ break LOOP
428
+ case <-errCh:
429
+ return nil, err
430
+ case <-timeout.C:
431
+ return nil, fmt.Errorf("time out")
432
+ }
433
+ }
434
+
435
+ // 不知道啥原因,oss那边分片上传不计算sha1,导致115服务器校验错误
436
+ // params.Callback.Callback = strings.ReplaceAll(params.Callback.Callback, "${sha1}", params.SHA1)
437
+ if _, err := bucket.CompleteMultipartUpload(imur, parts, append(
438
+ driver115.OssOption(params, ossToken),
439
+ oss.CallbackResult(&bodyBytes),
440
+ )...); err != nil {
441
+ return nil, err
442
+ }
443
+
444
+ var uploadResult UploadResult
445
+ if err = json.Unmarshal(bodyBytes, &uploadResult); err != nil {
446
+ return nil, err
447
+ }
448
+ return &uploadResult, uploadResult.Err(string(bodyBytes))
449
+ }
450
+
451
+ func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
452
+ for _, chunk := range chunks {
453
+ ch <- chunk
454
+ }
455
+ }
456
+
457
+ func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
458
+ for i := int64(1); i < 10; i++ {
459
+ if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
460
+ if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
461
+ return
462
+ }
463
+ break
464
+ }
465
+ }
466
+ if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
467
+ if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
468
+ return
469
+ }
470
+ }
471
+ // 单个分片大小不能小于100KB
472
+ if chunks[0].Size < 100*utils.KB {
473
+ if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
474
+ return
475
+ }
476
+ }
477
+ return
478
+ }
479
+
480
+ // SplitFileByPartNum splits big file into parts by the num of parts.
481
+ // Split the file with specified parts count, returns the split result when error is nil.
482
+ func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
483
+ if chunkNum <= 0 || chunkNum > 10000 {
484
+ return nil, errors.New("chunkNum invalid")
485
+ }
486
+
487
+ if int64(chunkNum) > fileSize {
488
+ return nil, errors.New("oss: chunkNum invalid")
489
+ }
490
+
491
+ var chunks []oss.FileChunk
492
+ chunk := oss.FileChunk{}
493
+ chunkN := (int64)(chunkNum)
494
+ for i := int64(0); i < chunkN; i++ {
495
+ chunk.Number = int(i + 1)
496
+ chunk.Offset = i * (fileSize / chunkN)
497
+ if i == chunkN-1 {
498
+ chunk.Size = fileSize/chunkN + fileSize%chunkN
499
+ } else {
500
+ chunk.Size = fileSize / chunkN
501
+ }
502
+ chunks = append(chunks, chunk)
503
+ }
504
+
505
+ return chunks, nil
506
+ }
507
+
508
+ // SplitFileByPartSize splits big file into parts by the size of parts.
509
+ // Splits the file by the part size. Returns the FileChunk when error is nil.
510
+ func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
511
+ if chunkSize <= 0 {
512
+ return nil, errors.New("chunkSize invalid")
513
+ }
514
+
515
+ chunkN := fileSize / chunkSize
516
+ if chunkN >= 10000 {
517
+ return nil, errors.New("Too many parts, please increase part size")
518
+ }
519
+
520
+ var chunks []oss.FileChunk
521
+ chunk := oss.FileChunk{}
522
+ for i := int64(0); i < chunkN; i++ {
523
+ chunk.Number = int(i + 1)
524
+ chunk.Offset = i * chunkSize
525
+ chunk.Size = chunkSize
526
+ chunks = append(chunks, chunk)
527
+ }
528
+
529
+ if fileSize%chunkSize > 0 {
530
+ chunk.Number = len(chunks) + 1
531
+ chunk.Offset = int64(len(chunks)) * chunkSize
532
+ chunk.Size = fileSize % chunkSize
533
+ chunks = append(chunks, chunk)
534
+ }
535
+
536
+ return chunks, nil
537
+ }
drivers/115_share/driver.go ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "context"
5
+
6
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
7
+ "github.com/alist-org/alist/v3/internal/driver"
8
+ "github.com/alist-org/alist/v3/internal/errs"
9
+ "github.com/alist-org/alist/v3/internal/model"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "golang.org/x/time/rate"
12
+ )
13
+
14
+ type Pan115Share struct {
15
+ model.Storage
16
+ Addition
17
+ client *driver115.Pan115Client
18
+ limiter *rate.Limiter
19
+ }
20
+
21
+ func (d *Pan115Share) Config() driver.Config {
22
+ return config
23
+ }
24
+
25
+ func (d *Pan115Share) GetAddition() driver.Additional {
26
+ return &d.Addition
27
+ }
28
+
29
+ func (d *Pan115Share) Init(ctx context.Context) error {
30
+ if d.LimitRate > 0 {
31
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
32
+ }
33
+
34
+ return d.login()
35
+ }
36
+
37
+ func (d *Pan115Share) WaitLimit(ctx context.Context) error {
38
+ if d.limiter != nil {
39
+ return d.limiter.Wait(ctx)
40
+ }
41
+ return nil
42
+ }
43
+
44
+ func (d *Pan115Share) Drop(ctx context.Context) error {
45
+ return nil
46
+ }
47
+
48
+ func (d *Pan115Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
49
+ if err := d.WaitLimit(ctx); err != nil {
50
+ return nil, err
51
+ }
52
+
53
+ files := make([]driver115.ShareFile, 0)
54
+ fileResp, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, dir.GetID(), driver115.QueryLimit(int(d.PageSize)))
55
+ if err != nil {
56
+ return nil, err
57
+ }
58
+ files = append(files, fileResp.Data.List...)
59
+ total := fileResp.Data.Count
60
+ count := len(fileResp.Data.List)
61
+ for total > count {
62
+ fileResp, err := d.client.GetShareSnap(
63
+ d.ShareCode, d.ReceiveCode, dir.GetID(),
64
+ driver115.QueryLimit(int(d.PageSize)), driver115.QueryOffset(count),
65
+ )
66
+ if err != nil {
67
+ return nil, err
68
+ }
69
+ files = append(files, fileResp.Data.List...)
70
+ count += len(fileResp.Data.List)
71
+ }
72
+
73
+ return utils.SliceConvert(files, transFunc)
74
+ }
75
+
76
+ func (d *Pan115Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
77
+ if err := d.WaitLimit(ctx); err != nil {
78
+ return nil, err
79
+ }
80
+ downloadInfo, err := d.client.DownloadByShareCode(d.ShareCode, d.ReceiveCode, file.GetID())
81
+ if err != nil {
82
+ return nil, err
83
+ }
84
+
85
+ return &model.Link{URL: downloadInfo.URL.URL}, nil
86
+ }
87
+
88
+ func (d *Pan115Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
89
+ return errs.NotSupport
90
+ }
91
+
92
+ func (d *Pan115Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
93
+ return errs.NotSupport
94
+ }
95
+
96
+ func (d *Pan115Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
97
+ return errs.NotSupport
98
+ }
99
+
100
+ func (d *Pan115Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
101
+ return errs.NotSupport
102
+ }
103
+
104
+ func (d *Pan115Share) Remove(ctx context.Context, obj model.Obj) error {
105
+ return errs.NotSupport
106
+ }
107
+
108
+ func (d *Pan115Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
109
+ return errs.NotSupport
110
+ }
111
+
112
+ var _ driver.Driver = (*Pan115Share)(nil)
drivers/115_share/meta.go ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Cookie string `json:"cookie" type:"text" help:"one of QR code token and cookie required"`
10
+ QRCodeToken string `json:"qrcode_token" type:"text" help:"one of QR code token and cookie required"`
11
+ QRCodeSource string `json:"qrcode_source" type:"select" options:"web,android,ios,tv,alipaymini,wechatmini,qandroid" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"1000" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"float" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
14
+ ShareCode string `json:"share_code" type:"text" required:"true" help:"share code of 115 share link"`
15
+ ReceiveCode string `json:"receive_code" type:"text" required:"true" help:"receive code of 115 share link"`
16
+ driver.RootID
17
+ }
18
+
19
+ var config = driver.Config{
20
+ Name: "115 Share",
21
+ DefaultRoot: "",
22
+ // OnlyProxy: true,
23
+ // OnlyLocal: true,
24
+ CheckStatus: false,
25
+ Alert: "",
26
+ NoOverwriteUpload: true,
27
+ NoUpload: true,
28
+ }
29
+
30
+ func init() {
31
+ op.RegisterDriver(func() driver.Driver {
32
+ return &Pan115Share{}
33
+ })
34
+ }
drivers/115_share/utils.go ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115_share
2
+
3
+ import (
4
+ "fmt"
5
+ "strconv"
6
+ "time"
7
+
8
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
9
+ "github.com/alist-org/alist/v3/internal/model"
10
+ "github.com/alist-org/alist/v3/pkg/utils"
11
+ "github.com/pkg/errors"
12
+ )
13
+
14
+ var _ model.Obj = (*FileObj)(nil)
15
+
16
+ type FileObj struct {
17
+ Size int64
18
+ Sha1 string
19
+ Utm time.Time
20
+ FileName string
21
+ isDir bool
22
+ FileID string
23
+ }
24
+
25
+ func (f *FileObj) CreateTime() time.Time {
26
+ return f.Utm
27
+ }
28
+
29
+ func (f *FileObj) GetHash() utils.HashInfo {
30
+ return utils.NewHashInfo(utils.SHA1, f.Sha1)
31
+ }
32
+
33
+ func (f *FileObj) GetSize() int64 {
34
+ return f.Size
35
+ }
36
+
37
+ func (f *FileObj) GetName() string {
38
+ return f.FileName
39
+ }
40
+
41
+ func (f *FileObj) ModTime() time.Time {
42
+ return f.Utm
43
+ }
44
+
45
+ func (f *FileObj) IsDir() bool {
46
+ return f.isDir
47
+ }
48
+
49
+ func (f *FileObj) GetID() string {
50
+ return f.FileID
51
+ }
52
+
53
+ func (f *FileObj) GetPath() string {
54
+ return ""
55
+ }
56
+
57
+ func transFunc(sf driver115.ShareFile) (model.Obj, error) {
58
+ timeInt, err := strconv.ParseInt(sf.UpdateTime, 10, 64)
59
+ if err != nil {
60
+ return nil, err
61
+ }
62
+ var (
63
+ utm = time.Unix(timeInt, 0)
64
+ isDir = (sf.IsFile == 0)
65
+ fileID = string(sf.FileID)
66
+ )
67
+ if isDir {
68
+ fileID = string(sf.CategoryID)
69
+ }
70
+ return &FileObj{
71
+ Size: int64(sf.Size),
72
+ Sha1: sf.Sha1,
73
+ Utm: utm,
74
+ FileName: string(sf.FileName),
75
+ isDir: isDir,
76
+ FileID: fileID,
77
+ }, nil
78
+ }
79
+
80
+ var UserAgent = driver115.UA115Browser
81
+
82
+ func (d *Pan115Share) login() error {
83
+ var err error
84
+ opts := []driver115.Option{
85
+ driver115.UA(UserAgent),
86
+ }
87
+ d.client = driver115.New(opts...)
88
+ if _, err := d.client.GetShareSnap(d.ShareCode, d.ReceiveCode, ""); err != nil {
89
+ return errors.Wrap(err, "failed to get share snap")
90
+ }
91
+ cr := &driver115.Credential{}
92
+ if d.QRCodeToken != "" {
93
+ s := &driver115.QRCodeSession{
94
+ UID: d.QRCodeToken,
95
+ }
96
+ if cr, err = d.client.QRCodeLoginWithApp(s, driver115.LoginApp(d.QRCodeSource)); err != nil {
97
+ return errors.Wrap(err, "failed to login by qrcode")
98
+ }
99
+ d.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s;KID=%s", cr.UID, cr.CID, cr.SEID, cr.KID)
100
+ d.QRCodeToken = ""
101
+ } else if d.Cookie != "" {
102
+ if err = cr.FromCookie(d.Cookie); err != nil {
103
+ return errors.Wrap(err, "failed to login by cookies")
104
+ }
105
+ d.client.ImportCredential(cr)
106
+ } else {
107
+ return errors.New("missing cookie or qrcode account")
108
+ }
109
+
110
+ return d.client.LoginCheck()
111
+ }
drivers/123/driver.go ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "crypto/md5"
6
+ "encoding/base64"
7
+ "encoding/hex"
8
+ "fmt"
9
+ "golang.org/x/time/rate"
10
+ "io"
11
+ "net/http"
12
+ "net/url"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/internal/driver"
18
+ "github.com/alist-org/alist/v3/internal/errs"
19
+ "github.com/alist-org/alist/v3/internal/model"
20
+ "github.com/alist-org/alist/v3/pkg/utils"
21
+ "github.com/aws/aws-sdk-go/aws"
22
+ "github.com/aws/aws-sdk-go/aws/credentials"
23
+ "github.com/aws/aws-sdk-go/aws/session"
24
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
25
+ "github.com/go-resty/resty/v2"
26
+ log "github.com/sirupsen/logrus"
27
+ )
28
+
29
+ type Pan123 struct {
30
+ model.Storage
31
+ Addition
32
+ apiRateLimit sync.Map
33
+ }
34
+
35
+ func (d *Pan123) Config() driver.Config {
36
+ return config
37
+ }
38
+
39
+ func (d *Pan123) GetAddition() driver.Additional {
40
+ return &d.Addition
41
+ }
42
+
43
+ func (d *Pan123) Init(ctx context.Context) error {
44
+ _, err := d.request(UserInfo, http.MethodGet, nil, nil)
45
+ return err
46
+ }
47
+
48
+ func (d *Pan123) Drop(ctx context.Context) error {
49
+ _, _ = d.request(Logout, http.MethodPost, func(req *resty.Request) {
50
+ req.SetBody(base.Json{})
51
+ }, nil)
52
+ return nil
53
+ }
54
+
55
+ func (d *Pan123) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
56
+ files, err := d.getFiles(ctx, dir.GetID(), dir.GetName())
57
+ if err != nil {
58
+ return nil, err
59
+ }
60
+ return utils.SliceConvert(files, func(src File) (model.Obj, error) {
61
+ return src, nil
62
+ })
63
+ }
64
+
65
+ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
66
+ if f, ok := file.(File); ok {
67
+ //var resp DownResp
68
+ var headers map[string]string
69
+ if !utils.IsLocalIPAddr(args.IP) {
70
+ headers = map[string]string{
71
+ //"X-Real-IP": "1.1.1.1",
72
+ "X-Forwarded-For": args.IP,
73
+ }
74
+ }
75
+ data := base.Json{
76
+ "driveId": 0,
77
+ "etag": f.Etag,
78
+ "fileId": f.FileId,
79
+ "fileName": f.FileName,
80
+ "s3keyFlag": f.S3KeyFlag,
81
+ "size": f.Size,
82
+ "type": f.Type,
83
+ }
84
+ resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
85
+
86
+ req.SetBody(data).SetHeaders(headers)
87
+ }, nil)
88
+ if err != nil {
89
+ return nil, err
90
+ }
91
+ downloadUrl := utils.Json.Get(resp, "data", "DownloadUrl").ToString()
92
+ u, err := url.Parse(downloadUrl)
93
+ if err != nil {
94
+ return nil, err
95
+ }
96
+ nu := u.Query().Get("params")
97
+ if nu != "" {
98
+ du, _ := base64.StdEncoding.DecodeString(nu)
99
+ u, err = url.Parse(string(du))
100
+ if err != nil {
101
+ return nil, err
102
+ }
103
+ }
104
+ u_ := u.String()
105
+ log.Debug("download url: ", u_)
106
+ res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
107
+ if err != nil {
108
+ return nil, err
109
+ }
110
+ log.Debug(res.String())
111
+ link := model.Link{
112
+ URL: u_,
113
+ }
114
+ log.Debugln("res code: ", res.StatusCode())
115
+ if res.StatusCode() == 302 {
116
+ link.URL = res.Header().Get("location")
117
+ } else if res.StatusCode() < 300 {
118
+ link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
119
+ }
120
+ link.Header = http.Header{
121
+ "Referer": []string{"https://www.123pan.com/"},
122
+ }
123
+ return &link, nil
124
+ } else {
125
+ return nil, fmt.Errorf("can't convert obj")
126
+ }
127
+ }
128
+
129
+ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
130
+ data := base.Json{
131
+ "driveId": 0,
132
+ "etag": "",
133
+ "fileName": dirName,
134
+ "parentFileId": parentDir.GetID(),
135
+ "size": 0,
136
+ "type": 1,
137
+ }
138
+ _, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
139
+ req.SetBody(data)
140
+ }, nil)
141
+ return err
142
+ }
143
+
144
+ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
145
+ data := base.Json{
146
+ "fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
147
+ "parentFileId": dstDir.GetID(),
148
+ }
149
+ _, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
150
+ req.SetBody(data)
151
+ }, nil)
152
+ return err
153
+ }
154
+
155
+ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
156
+ data := base.Json{
157
+ "driveId": 0,
158
+ "fileId": srcObj.GetID(),
159
+ "fileName": newName,
160
+ }
161
+ _, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
162
+ req.SetBody(data)
163
+ }, nil)
164
+ return err
165
+ }
166
+
167
+ func (d *Pan123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
168
+ return errs.NotSupport
169
+ }
170
+
171
+ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
172
+ if f, ok := obj.(File); ok {
173
+ data := base.Json{
174
+ "driveId": 0,
175
+ "operation": true,
176
+ "fileTrashInfoList": []File{f},
177
+ }
178
+ _, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
179
+ req.SetBody(data)
180
+ }, nil)
181
+ return err
182
+ } else {
183
+ return fmt.Errorf("can't convert obj")
184
+ }
185
+ }
186
+
187
+ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
188
+ // const DEFAULT int64 = 10485760
189
+ h := md5.New()
190
+ // need to calculate md5 of the full content
191
+ tempFile, err := stream.CacheFullInTempFile()
192
+ if err != nil {
193
+ return err
194
+ }
195
+ defer func() {
196
+ _ = tempFile.Close()
197
+ }()
198
+ if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
199
+ return err
200
+ }
201
+ _, err = tempFile.Seek(0, io.SeekStart)
202
+ if err != nil {
203
+ return err
204
+ }
205
+ etag := hex.EncodeToString(h.Sum(nil))
206
+ data := base.Json{
207
+ "driveId": 0,
208
+ "duplicate": 2, // 2->覆盖 1->重命名 0->默认
209
+ "etag": etag,
210
+ "fileName": stream.GetName(),
211
+ "parentFileId": dstDir.GetID(),
212
+ "size": stream.GetSize(),
213
+ "type": 0,
214
+ }
215
+ var resp UploadResp
216
+ res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
217
+ req.SetBody(data).SetContext(ctx)
218
+ }, &resp)
219
+ if err != nil {
220
+ return err
221
+ }
222
+ log.Debugln("upload request res: ", string(res))
223
+ if resp.Data.Reuse || resp.Data.Key == "" {
224
+ return nil
225
+ }
226
+ if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
227
+ err = d.newUpload(ctx, &resp, stream, tempFile, up)
228
+ return err
229
+ } else {
230
+ cfg := &aws.Config{
231
+ Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
232
+ Region: aws.String("123pan"),
233
+ Endpoint: aws.String(resp.Data.EndPoint),
234
+ S3ForcePathStyle: aws.Bool(true),
235
+ }
236
+ s, err := session.NewSession(cfg)
237
+ if err != nil {
238
+ return err
239
+ }
240
+ uploader := s3manager.NewUploader(s)
241
+ if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
242
+ uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
243
+ }
244
+ input := &s3manager.UploadInput{
245
+ Bucket: &resp.Data.Bucket,
246
+ Key: &resp.Data.Key,
247
+ Body: tempFile,
248
+ }
249
+ _, err = uploader.UploadWithContext(ctx, input)
250
+ }
251
+ _, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
252
+ req.SetBody(base.Json{
253
+ "fileId": resp.Data.FileId,
254
+ }).SetContext(ctx)
255
+ }, nil)
256
+ return err
257
+ }
258
+
259
+ func (d *Pan123) APIRateLimit(ctx context.Context, api string) error {
260
+ value, _ := d.apiRateLimit.LoadOrStore(api,
261
+ rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
262
+ limiter := value.(*rate.Limiter)
263
+
264
+ return limiter.Wait(ctx)
265
+ }
266
+
267
+ var _ driver.Driver = (*Pan123)(nil)
drivers/123/meta.go ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ Username string `json:"username" required:"true"`
10
+ Password string `json:"password" required:"true"`
11
+ driver.RootID
12
+ //OrderBy string `json:"order_by" type:"select" options:"file_id,file_name,size,update_at" default:"file_name"`
13
+ //OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
14
+ AccessToken string
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "123Pan",
19
+ DefaultRoot: "0",
20
+ LocalSort: true,
21
+ }
22
+
23
+ func init() {
24
+ op.RegisterDriver(func() driver.Driver {
25
+ return &Pan123{}
26
+ })
27
+ }
drivers/123/types.go ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/pkg/utils"
5
+ "net/url"
6
+ "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/alist-org/alist/v3/internal/model"
12
+ )
13
+
14
+ type File struct {
15
+ FileName string `json:"FileName"`
16
+ Size int64 `json:"Size"`
17
+ UpdateAt time.Time `json:"UpdateAt"`
18
+ FileId int64 `json:"FileId"`
19
+ Type int `json:"Type"`
20
+ Etag string `json:"Etag"`
21
+ S3KeyFlag string `json:"S3KeyFlag"`
22
+ DownloadUrl string `json:"DownloadUrl"`
23
+ }
24
+
25
+ func (f File) CreateTime() time.Time {
26
+ return f.UpdateAt
27
+ }
28
+
29
+ func (f File) GetHash() utils.HashInfo {
30
+ return utils.HashInfo{}
31
+ }
32
+
33
+ func (f File) GetPath() string {
34
+ return ""
35
+ }
36
+
37
+ func (f File) GetSize() int64 {
38
+ return f.Size
39
+ }
40
+
41
+ func (f File) GetName() string {
42
+ return f.FileName
43
+ }
44
+
45
+ func (f File) ModTime() time.Time {
46
+ return f.UpdateAt
47
+ }
48
+
49
+ func (f File) IsDir() bool {
50
+ return f.Type == 1
51
+ }
52
+
53
+ func (f File) GetID() string {
54
+ return strconv.FormatInt(f.FileId, 10)
55
+ }
56
+
57
+ func (f File) Thumb() string {
58
+ if f.DownloadUrl == "" {
59
+ return ""
60
+ }
61
+ du, err := url.Parse(f.DownloadUrl)
62
+ if err != nil {
63
+ return ""
64
+ }
65
+ du.Path = strings.TrimSuffix(du.Path, "_24_24") + "_70_70"
66
+ query := du.Query()
67
+ query.Set("w", "70")
68
+ query.Set("h", "70")
69
+ if !query.Has("type") {
70
+ query.Set("type", strings.TrimPrefix(path.Base(f.FileName), "."))
71
+ }
72
+ if !query.Has("trade_key") {
73
+ query.Set("trade_key", "123pan-thumbnail")
74
+ }
75
+ du.RawQuery = query.Encode()
76
+ return du.String()
77
+ }
78
+
79
+ var _ model.Obj = (*File)(nil)
80
+ var _ model.Thumb = (*File)(nil)
81
+
82
+ //func (f File) Thumb() string {
83
+ //
84
+ //}
85
+ //var _ model.Thumb = (*File)(nil)
86
+
87
+ type Files struct {
88
+ //BaseResp
89
+ Data struct {
90
+ Next string `json:"Next"`
91
+ Total int `json:"Total"`
92
+ InfoList []File `json:"InfoList"`
93
+ } `json:"data"`
94
+ }
95
+
96
+ //type DownResp struct {
97
+ // //BaseResp
98
+ // Data struct {
99
+ // DownloadUrl string `json:"DownloadUrl"`
100
+ // } `json:"data"`
101
+ //}
102
+
103
+ type UploadResp struct {
104
+ //BaseResp
105
+ Data struct {
106
+ AccessKeyId string `json:"AccessKeyId"`
107
+ Bucket string `json:"Bucket"`
108
+ Key string `json:"Key"`
109
+ SecretAccessKey string `json:"SecretAccessKey"`
110
+ SessionToken string `json:"SessionToken"`
111
+ FileId int64 `json:"FileId"`
112
+ Reuse bool `json:"Reuse"`
113
+ EndPoint string `json:"EndPoint"`
114
+ StorageNode string `json:"StorageNode"`
115
+ UploadId string `json:"UploadId"`
116
+ } `json:"data"`
117
+ }
118
+
119
+ type S3PreSignedURLs struct {
120
+ Data struct {
121
+ PreSignedUrls map[string]string `json:"presignedUrls"`
122
+ } `json:"data"`
123
+ }
drivers/123/upload.go ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "io"
7
+ "math"
8
+ "net/http"
9
+ "strconv"
10
+
11
+ "github.com/alist-org/alist/v3/drivers/base"
12
+ "github.com/alist-org/alist/v3/internal/driver"
13
+ "github.com/alist-org/alist/v3/internal/model"
14
+ "github.com/alist-org/alist/v3/pkg/utils"
15
+ "github.com/go-resty/resty/v2"
16
+ )
17
+
18
+ func (d *Pan123) getS3PreSignedUrls(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
19
+ data := base.Json{
20
+ "bucket": upReq.Data.Bucket,
21
+ "key": upReq.Data.Key,
22
+ "partNumberEnd": end,
23
+ "partNumberStart": start,
24
+ "uploadId": upReq.Data.UploadId,
25
+ "StorageNode": upReq.Data.StorageNode,
26
+ }
27
+ var s3PreSignedUrls S3PreSignedURLs
28
+ _, err := d.request(S3PreSignedUrls, http.MethodPost, func(req *resty.Request) {
29
+ req.SetBody(data).SetContext(ctx)
30
+ }, &s3PreSignedUrls)
31
+ if err != nil {
32
+ return nil, err
33
+ }
34
+ return &s3PreSignedUrls, nil
35
+ }
36
+
37
+ func (d *Pan123) getS3Auth(ctx context.Context, upReq *UploadResp, start, end int) (*S3PreSignedURLs, error) {
38
+ data := base.Json{
39
+ "StorageNode": upReq.Data.StorageNode,
40
+ "bucket": upReq.Data.Bucket,
41
+ "key": upReq.Data.Key,
42
+ "partNumberEnd": end,
43
+ "partNumberStart": start,
44
+ "uploadId": upReq.Data.UploadId,
45
+ }
46
+ var s3PreSignedUrls S3PreSignedURLs
47
+ _, err := d.request(S3Auth, http.MethodPost, func(req *resty.Request) {
48
+ req.SetBody(data).SetContext(ctx)
49
+ }, &s3PreSignedUrls)
50
+ if err != nil {
51
+ return nil, err
52
+ }
53
+ return &s3PreSignedUrls, nil
54
+ }
55
+
56
+ func (d *Pan123) completeS3(ctx context.Context, upReq *UploadResp, file model.FileStreamer, isMultipart bool) error {
57
+ data := base.Json{
58
+ "StorageNode": upReq.Data.StorageNode,
59
+ "bucket": upReq.Data.Bucket,
60
+ "fileId": upReq.Data.FileId,
61
+ "fileSize": file.GetSize(),
62
+ "isMultipart": isMultipart,
63
+ "key": upReq.Data.Key,
64
+ "uploadId": upReq.Data.UploadId,
65
+ }
66
+ _, err := d.request(UploadCompleteV2, http.MethodPost, func(req *resty.Request) {
67
+ req.SetBody(data).SetContext(ctx)
68
+ }, nil)
69
+ return err
70
+ }
71
+
72
+ func (d *Pan123) newUpload(ctx context.Context, upReq *UploadResp, file model.FileStreamer, reader io.Reader, up driver.UpdateProgress) error {
73
+ chunkSize := int64(1024 * 1024 * 16)
74
+ // fetch s3 pre signed urls
75
+ chunkCount := int(math.Ceil(float64(file.GetSize()) / float64(chunkSize)))
76
+ // only 1 batch is allowed
77
+ isMultipart := chunkCount > 1
78
+ batchSize := 1
79
+ getS3UploadUrl := d.getS3Auth
80
+ if isMultipart {
81
+ batchSize = 10
82
+ getS3UploadUrl = d.getS3PreSignedUrls
83
+ }
84
+ for i := 1; i <= chunkCount; i += batchSize {
85
+ if utils.IsCanceled(ctx) {
86
+ return ctx.Err()
87
+ }
88
+ start := i
89
+ end := i + batchSize
90
+ if end > chunkCount+1 {
91
+ end = chunkCount + 1
92
+ }
93
+ s3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, start, end)
94
+ if err != nil {
95
+ return err
96
+ }
97
+ // upload each chunk
98
+ for j := start; j < end; j++ {
99
+ if utils.IsCanceled(ctx) {
100
+ return ctx.Err()
101
+ }
102
+ curSize := chunkSize
103
+ if j == chunkCount {
104
+ curSize = file.GetSize() - (int64(chunkCount)-1)*chunkSize
105
+ }
106
+ err = d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, j, end, io.LimitReader(reader, chunkSize), curSize, false, getS3UploadUrl)
107
+ if err != nil {
108
+ return err
109
+ }
110
+ up(float64(j) * 100 / float64(chunkCount))
111
+ }
112
+ }
113
+ // complete s3 upload
114
+ return d.completeS3(ctx, upReq, file, chunkCount > 1)
115
+ }
116
+
117
+ func (d *Pan123) uploadS3Chunk(ctx context.Context, upReq *UploadResp, s3PreSignedUrls *S3PreSignedURLs, cur, end int, reader io.Reader, curSize int64, retry bool, getS3UploadUrl func(ctx context.Context, upReq *UploadResp, start int, end int) (*S3PreSignedURLs, error)) error {
118
+ uploadUrl := s3PreSignedUrls.Data.PreSignedUrls[strconv.Itoa(cur)]
119
+ if uploadUrl == "" {
120
+ return fmt.Errorf("upload url is empty, s3PreSignedUrls: %+v", s3PreSignedUrls)
121
+ }
122
+ req, err := http.NewRequest("PUT", uploadUrl, reader)
123
+ if err != nil {
124
+ return err
125
+ }
126
+ req = req.WithContext(ctx)
127
+ req.ContentLength = curSize
128
+ //req.Header.Set("Content-Length", strconv.FormatInt(curSize, 10))
129
+ res, err := base.HttpClient.Do(req)
130
+ if err != nil {
131
+ return err
132
+ }
133
+ defer res.Body.Close()
134
+ if res.StatusCode == http.StatusForbidden {
135
+ if retry {
136
+ return fmt.Errorf("upload s3 chunk %d failed, status code: %d", cur, res.StatusCode)
137
+ }
138
+ // refresh s3 pre signed urls
139
+ newS3PreSignedUrls, err := getS3UploadUrl(ctx, upReq, cur, end)
140
+ if err != nil {
141
+ return err
142
+ }
143
+ s3PreSignedUrls.Data.PreSignedUrls = newS3PreSignedUrls.Data.PreSignedUrls
144
+ // retry
145
+ return d.uploadS3Chunk(ctx, upReq, s3PreSignedUrls, cur, end, reader, curSize, true, getS3UploadUrl)
146
+ }
147
+ if res.StatusCode != http.StatusOK {
148
+ body, err := io.ReadAll(res.Body)
149
+ if err != nil {
150
+ return err
151
+ }
152
+ return fmt.Errorf("upload s3 chunk %d failed, status code: %d, body: %s", cur, res.StatusCode, body)
153
+ }
154
+ return nil
155
+ }
drivers/123/util.go ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "hash/crc32"
8
+ "math"
9
+ "math/rand"
10
+ "net/http"
11
+ "net/url"
12
+ "strconv"
13
+ "strings"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ jsoniter "github.com/json-iterator/go"
20
+ log "github.com/sirupsen/logrus"
21
+ )
22
+
23
+ // do others that not defined in Driver interface
24
+
25
+ const (
26
+ Api = "https://www.123pan.com/api"
27
+ AApi = "https://www.123pan.com/a/api"
28
+ BApi = "https://www.123pan.com/b/api"
29
+ LoginApi = "https://login.123pan.com/api"
30
+ MainApi = BApi
31
+ SignIn = LoginApi + "/user/sign_in"
32
+ Logout = MainApi + "/user/logout"
33
+ UserInfo = MainApi + "/user/info"
34
+ FileList = MainApi + "/file/list/new"
35
+ DownloadInfo = MainApi + "/file/download_info"
36
+ Mkdir = MainApi + "/file/upload_request"
37
+ Move = MainApi + "/file/mod_pid"
38
+ Rename = MainApi + "/file/rename"
39
+ Trash = MainApi + "/file/trash"
40
+ UploadRequest = MainApi + "/file/upload_request"
41
+ UploadComplete = MainApi + "/file/upload_complete"
42
+ S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
43
+ S3Auth = MainApi + "/file/s3_upload_object/auth"
44
+ UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
45
+ S3Complete = MainApi + "/file/s3_complete_multipart_upload"
46
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
47
+ )
48
+
49
+ func signPath(path string, os string, version string) (k string, v string) {
50
+ table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
51
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
52
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
53
+ timestamp := fmt.Sprint(now.Unix())
54
+ nowStr := []byte(now.Format("200601021504"))
55
+ for i := 0; i < len(nowStr); i++ {
56
+ nowStr[i] = table[nowStr[i]-48]
57
+ }
58
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
59
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
60
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
61
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
62
+ }
63
+
64
+ func GetApi(rawUrl string) string {
65
+ u, _ := url.Parse(rawUrl)
66
+ query := u.Query()
67
+ query.Add(signPath(u.Path, "web", "3"))
68
+ u.RawQuery = query.Encode()
69
+ return u.String()
70
+ }
71
+
72
+ //func GetApi(url string) string {
73
+ // vm := js.New()
74
+ // vm.Set("url", url[22:])
75
+ // r, err := vm.RunString(`
76
+ // (function(e){
77
+ // function A(t, e) {
78
+ // e = 1 < arguments.length && void 0 !== e ? e : 10;
79
+ // for (var n = function() {
80
+ // for (var t = [], e = 0; e < 256; e++) {
81
+ // for (var n = e, r = 0; r < 8; r++)
82
+ // n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;
83
+ // t[e] = n
84
+ // }
85
+ // return t
86
+ // }(), r = function(t) {
87
+ // t = t.replace(/\\r\\n/g, "\\n");
88
+ // for (var e = "", n = 0; n < t.length; n++) {
89
+ // var r = t.charCodeAt(n);
90
+ // r < 128 ? e += String.fromCharCode(r) : e = 127 < r && r < 2048 ? (e += String.fromCharCode(r >> 6 | 192)) + String.fromCharCode(63 & r | 128) : (e = (e += String.fromCharCode(r >> 12 | 224)) + String.fromCharCode(r >> 6 & 63 | 128)) + String.fromCharCode(63 & r | 128)
91
+ // }
92
+ // return e
93
+ // }(t), a = -1, i = 0; i < r.length; i++)
94
+ // a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];
95
+ // return (a = (-1 ^ a) >>> 0).toString(e)
96
+ // }
97
+ //
98
+ // function v(t) {
99
+ // return (v = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(t) {
100
+ // return typeof t
101
+ // }
102
+ // : function(t) {
103
+ // return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
104
+ // }
105
+ // )(t)
106
+ // }
107
+ //
108
+ // for (p in a = Math.round(1e7 * Math.random()),
109
+ // o = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),
110
+ // m = ["a", "d", "e", "f", "g", "h", "l", "m", "y", "i", "j", "n", "o", "p", "k", "q", "r", "s", "t", "u", "b", "c", "v", "w", "s", "z"],
111
+ // u = function(t, e, n) {
112
+ // var r;
113
+ // n = 2 < arguments.length && void 0 !== n ? n : 8;
114
+ // return 0 === arguments.length ? null : (r = "object" === v(t) ? t : (10 === "".concat(t).length && (t = 1e3 * Number.parseInt(t)),
115
+ // new Date(t)),
116
+ // t += 6e4 * new Date(t).getTimezoneOffset(),
117
+ // {
118
+ // y: (r = new Date(t + 36e5 * n)).getFullYear(),
119
+ // m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1,
120
+ // d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(),
121
+ // h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(),
122
+ // f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes()
123
+ // })
124
+ // }(o),
125
+ // h = u.y,
126
+ // g = u.m,
127
+ // l = u.d,
128
+ // c = u.h,
129
+ // u = u.f,
130
+ // d = [h, g, l, c, u].join(""),
131
+ // f = [],
132
+ // d)
133
+ // f.push(m[Number(d[p])]);
134
+ // return h = A(f.join("")),
135
+ // g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat("web", "|").concat("3", "|").concat(h)),
136
+ // "".concat(h, "=").concat(o, "-").concat(a, "-").concat(g);
137
+ // })(url)
138
+ // `)
139
+ // if err != nil {
140
+ // fmt.Println(err)
141
+ // return url
142
+ // }
143
+ // v, _ := r.Export().(string)
144
+ // return url + "?" + v
145
+ //}
146
+
147
+ func (d *Pan123) login() error {
148
+ var body base.Json
149
+ if utils.IsEmailFormat(d.Username) {
150
+ body = base.Json{
151
+ "mail": d.Username,
152
+ "password": d.Password,
153
+ "type": 2,
154
+ }
155
+ } else {
156
+ body = base.Json{
157
+ "passport": d.Username,
158
+ "password": d.Password,
159
+ "remember": true,
160
+ }
161
+ }
162
+ res, err := base.RestyClient.R().
163
+ SetHeaders(map[string]string{
164
+ "origin": "https://www.123pan.com",
165
+ "referer": "https://www.123pan.com/",
166
+ "user-agent": "Dart/2.19(dart:io)-alist",
167
+ "platform": "web",
168
+ "app-version": "3",
169
+ //"user-agent": base.UserAgent,
170
+ }).
171
+ SetBody(body).Post(SignIn)
172
+ if err != nil {
173
+ return err
174
+ }
175
+ if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
176
+ err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
177
+ } else {
178
+ d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
179
+ }
180
+ return err
181
+ }
182
+
183
+ //func authKey(reqUrl string) (*string, error) {
184
+ // reqURL, err := url.Parse(reqUrl)
185
+ // if err != nil {
186
+ // return nil, err
187
+ // }
188
+ //
189
+ // nowUnix := time.Now().Unix()
190
+ // random := rand.Intn(0x989680)
191
+ //
192
+ // p4 := fmt.Sprintf("%d|%d|%s|%s|%s|%s", nowUnix, random, reqURL.Path, "web", "3", AuthKeySalt)
193
+ // authKey := fmt.Sprintf("%d-%d-%x", nowUnix, random, md5.Sum([]byte(p4)))
194
+ // return &authKey, nil
195
+ //}
196
+
197
+ func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
198
+ req := base.RestyClient.R()
199
+ req.SetHeaders(map[string]string{
200
+ "origin": "https://www.123pan.com",
201
+ "referer": "https://www.123pan.com/",
202
+ "authorization": "Bearer " + d.AccessToken,
203
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
204
+ "platform": "web",
205
+ "app-version": "3",
206
+ //"user-agent": base.UserAgent,
207
+ })
208
+ if callback != nil {
209
+ callback(req)
210
+ }
211
+ if resp != nil {
212
+ req.SetResult(resp)
213
+ }
214
+ //authKey, err := authKey(url)
215
+ //if err != nil {
216
+ // return nil, err
217
+ //}
218
+ //req.SetQueryParam("auth-key", *authKey)
219
+ res, err := req.Execute(method, GetApi(url))
220
+ if err != nil {
221
+ return nil, err
222
+ }
223
+ body := res.Body()
224
+ code := utils.Json.Get(body, "code").ToInt()
225
+ if code != 0 {
226
+ if code == 401 {
227
+ err := d.login()
228
+ if err != nil {
229
+ return nil, err
230
+ }
231
+ return d.request(url, method, callback, resp)
232
+ }
233
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
234
+ }
235
+ return body, nil
236
+ }
237
+
238
+ func (d *Pan123) getFiles(ctx context.Context, parentId string, name string) ([]File, error) {
239
+ page := 1
240
+ total := 0
241
+ res := make([]File, 0)
242
+ // 2024-02-06 fix concurrency by 123pan
243
+ for {
244
+ if err := d.APIRateLimit(ctx, FileList); err != nil {
245
+ return nil, err
246
+ }
247
+ var resp Files
248
+ query := map[string]string{
249
+ "driveId": "0",
250
+ "limit": "100",
251
+ "next": "0",
252
+ "orderBy": "file_id",
253
+ "orderDirection": "desc",
254
+ "parentFileId": parentId,
255
+ "trashed": "false",
256
+ "SearchData": "",
257
+ "Page": strconv.Itoa(page),
258
+ "OnlyLookAbnormalFile": "0",
259
+ "event": "homeListFile",
260
+ "operateType": "4",
261
+ "inDirectSpace": "false",
262
+ }
263
+ _res, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
264
+ req.SetQueryParams(query)
265
+ }, &resp)
266
+ if err != nil {
267
+ return nil, err
268
+ }
269
+ log.Debug(string(_res))
270
+ page++
271
+ res = append(res, resp.Data.InfoList...)
272
+ total = resp.Data.Total
273
+ if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
274
+ break
275
+ }
276
+ }
277
+ if len(res) != total {
278
+ log.Warnf("incorrect file count from remote at %s: expected %d, got %d", name, total, len(res))
279
+ }
280
+ return res, nil
281
+ }
drivers/123_link/driver.go ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "context"
5
+ stdpath "path"
6
+ "time"
7
+
8
+ "github.com/alist-org/alist/v3/internal/driver"
9
+ "github.com/alist-org/alist/v3/internal/errs"
10
+ "github.com/alist-org/alist/v3/internal/model"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ )
13
+
14
+ type Pan123Link struct {
15
+ model.Storage
16
+ Addition
17
+ root *Node
18
+ }
19
+
20
+ func (d *Pan123Link) Config() driver.Config {
21
+ return config
22
+ }
23
+
24
+ func (d *Pan123Link) GetAddition() driver.Additional {
25
+ return &d.Addition
26
+ }
27
+
28
+ func (d *Pan123Link) Init(ctx context.Context) error {
29
+ node, err := BuildTree(d.OriginURLs)
30
+ if err != nil {
31
+ return err
32
+ }
33
+ node.calSize()
34
+ d.root = node
35
+ return nil
36
+ }
37
+
38
+ func (d *Pan123Link) Drop(ctx context.Context) error {
39
+ return nil
40
+ }
41
+
42
+ func (d *Pan123Link) Get(ctx context.Context, path string) (model.Obj, error) {
43
+ node := GetNodeFromRootByPath(d.root, path)
44
+ return nodeToObj(node, path)
45
+ }
46
+
47
+ func (d *Pan123Link) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
48
+ node := GetNodeFromRootByPath(d.root, dir.GetPath())
49
+ if node == nil {
50
+ return nil, errs.ObjectNotFound
51
+ }
52
+ if node.isFile() {
53
+ return nil, errs.NotFolder
54
+ }
55
+ return utils.SliceConvert(node.Children, func(node *Node) (model.Obj, error) {
56
+ return nodeToObj(node, stdpath.Join(dir.GetPath(), node.Name))
57
+ })
58
+ }
59
+
60
+ func (d *Pan123Link) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
61
+ node := GetNodeFromRootByPath(d.root, file.GetPath())
62
+ if node == nil {
63
+ return nil, errs.ObjectNotFound
64
+ }
65
+ if node.isFile() {
66
+ signUrl, err := SignURL(node.Url, d.PrivateKey, d.UID, time.Duration(d.ValidDuration)*time.Minute)
67
+ if err != nil {
68
+ return nil, err
69
+ }
70
+ return &model.Link{
71
+ URL: signUrl,
72
+ }, nil
73
+ }
74
+ return nil, errs.NotFile
75
+ }
76
+
77
+ var _ driver.Driver = (*Pan123Link)(nil)
drivers/123_link/meta.go ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ OriginURLs string `json:"origin_urls" type:"text" required:"true" default:"https://vip.123pan.com/29/folder/file.mp3" help:"structure:FolderName:\n [FileSize:][Modified:]Url"`
10
+ PrivateKey string `json:"private_key"`
11
+ UID uint64 `json:"uid" type:"number"`
12
+ ValidDuration int64 `json:"valid_duration" type:"number" default:"30" help:"minutes"`
13
+ }
14
+
15
+ var config = driver.Config{
16
+ Name: "123PanLink",
17
+ }
18
+
19
+ func init() {
20
+ op.RegisterDriver(func() driver.Driver {
21
+ return &Pan123Link{}
22
+ })
23
+ }
drivers/123_link/parse.go ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "fmt"
5
+ url2 "net/url"
6
+ stdpath "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+ )
11
+
12
+ // build tree from text, text structure definition:
13
+ /**
14
+ * FolderName:
15
+ * [FileSize:][Modified:]Url
16
+ */
17
+ /**
18
+ * For example:
19
+ * folder1:
20
+ * name1:url1
21
+ * url2
22
+ * folder2:
23
+ * url3
24
+ * url4
25
+ * url5
26
+ * folder3:
27
+ * url6
28
+ * url7
29
+ * url8
30
+ */
31
+ // if there are no name, use the last segment of url as name
32
+ func BuildTree(text string) (*Node, error) {
33
+ lines := strings.Split(text, "\n")
34
+ var root = &Node{Level: -1, Name: "root"}
35
+ stack := []*Node{root}
36
+ for _, line := range lines {
37
+ // calculate indent
38
+ indent := 0
39
+ for i := 0; i < len(line); i++ {
40
+ if line[i] != ' ' {
41
+ break
42
+ }
43
+ indent++
44
+ }
45
+ // if indent is not a multiple of 2, it is an error
46
+ if indent%2 != 0 {
47
+ return nil, fmt.Errorf("the line '%s' is not a multiple of 2", line)
48
+ }
49
+ // calculate level
50
+ level := indent / 2
51
+ line = strings.TrimSpace(line[indent:])
52
+ // if the line is empty, skip
53
+ if line == "" {
54
+ continue
55
+ }
56
+ // if level isn't greater than the level of the top of the stack
57
+ // it is not the child of the top of the stack
58
+ for level <= stack[len(stack)-1].Level {
59
+ // pop the top of the stack
60
+ stack = stack[:len(stack)-1]
61
+ }
62
+ // if the line is a folder
63
+ if isFolder(line) {
64
+ // create a new node
65
+ node := &Node{
66
+ Level: level,
67
+ Name: strings.TrimSuffix(line, ":"),
68
+ }
69
+ // add the node to the top of the stack
70
+ stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
71
+ // push the node to the stack
72
+ stack = append(stack, node)
73
+ } else {
74
+ // if the line is a file
75
+ // create a new node
76
+ node, err := parseFileLine(line)
77
+ if err != nil {
78
+ return nil, err
79
+ }
80
+ node.Level = level
81
+ // add the node to the top of the stack
82
+ stack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node)
83
+ }
84
+ }
85
+ return root, nil
86
+ }
87
+
88
+ func isFolder(line string) bool {
89
+ return strings.HasSuffix(line, ":")
90
+ }
91
+
92
+ // line definition:
93
+ // [FileSize:][Modified:]Url
94
+ func parseFileLine(line string) (*Node, error) {
95
+ // if there is no url, it is an error
96
+ if !strings.Contains(line, "http://") && !strings.Contains(line, "https://") {
97
+ return nil, fmt.Errorf("invalid line: %s, because url is required for file", line)
98
+ }
99
+ index := strings.Index(line, "http://")
100
+ if index == -1 {
101
+ index = strings.Index(line, "https://")
102
+ }
103
+ url := line[index:]
104
+ info := line[:index]
105
+ node := &Node{
106
+ Url: url,
107
+ }
108
+ name := stdpath.Base(url)
109
+ unescape, err := url2.PathUnescape(name)
110
+ if err == nil {
111
+ name = unescape
112
+ }
113
+ node.Name = name
114
+ if index > 0 {
115
+ if !strings.HasSuffix(info, ":") {
116
+ return nil, fmt.Errorf("invalid line: %s, because file info must end with ':'", line)
117
+ }
118
+ info = info[:len(info)-1]
119
+ if info == "" {
120
+ return nil, fmt.Errorf("invalid line: %s, because file name can't be empty", line)
121
+ }
122
+ infoParts := strings.Split(info, ":")
123
+ size, err := strconv.ParseInt(infoParts[0], 10, 64)
124
+ if err != nil {
125
+ return nil, fmt.Errorf("invalid line: %s, because file size must be an integer", line)
126
+ }
127
+ node.Size = size
128
+ if len(infoParts) > 1 {
129
+ modified, err := strconv.ParseInt(infoParts[1], 10, 64)
130
+ if err != nil {
131
+ return nil, fmt.Errorf("invalid line: %s, because file modified must be an unix timestamp", line)
132
+ }
133
+ node.Modified = modified
134
+ } else {
135
+ node.Modified = time.Now().Unix()
136
+ }
137
+ }
138
+ return node, nil
139
+ }
140
+
141
+ func splitPath(path string) []string {
142
+ if path == "/" {
143
+ return []string{"root"}
144
+ }
145
+ parts := strings.Split(path, "/")
146
+ parts[0] = "root"
147
+ return parts
148
+ }
149
+
150
+ func GetNodeFromRootByPath(root *Node, path string) *Node {
151
+ return root.getByPath(splitPath(path))
152
+ }
drivers/123_link/types.go ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "time"
5
+
6
+ "github.com/alist-org/alist/v3/internal/errs"
7
+ "github.com/alist-org/alist/v3/internal/model"
8
+ )
9
+
10
+ // Node is a node in the folder tree
11
+ type Node struct {
12
+ Url string
13
+ Name string
14
+ Level int
15
+ Modified int64
16
+ Size int64
17
+ Children []*Node
18
+ }
19
+
20
+ func (node *Node) getByPath(paths []string) *Node {
21
+ if len(paths) == 0 || node == nil {
22
+ return nil
23
+ }
24
+ if node.Name != paths[0] {
25
+ return nil
26
+ }
27
+ if len(paths) == 1 {
28
+ return node
29
+ }
30
+ for _, child := range node.Children {
31
+ tmp := child.getByPath(paths[1:])
32
+ if tmp != nil {
33
+ return tmp
34
+ }
35
+ }
36
+ return nil
37
+ }
38
+
39
+ func (node *Node) isFile() bool {
40
+ return node.Url != ""
41
+ }
42
+
43
+ func (node *Node) calSize() int64 {
44
+ if node.isFile() {
45
+ return node.Size
46
+ }
47
+ var size int64 = 0
48
+ for _, child := range node.Children {
49
+ size += child.calSize()
50
+ }
51
+ node.Size = size
52
+ return size
53
+ }
54
+
55
+ func nodeToObj(node *Node, path string) (model.Obj, error) {
56
+ if node == nil {
57
+ return nil, errs.ObjectNotFound
58
+ }
59
+ return &model.Object{
60
+ Name: node.Name,
61
+ Size: node.Size,
62
+ Modified: time.Unix(node.Modified, 0),
63
+ IsFolder: !node.isFile(),
64
+ Path: path,
65
+ }, nil
66
+ }
drivers/123_link/util.go ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Link
2
+
3
+ import (
4
+ "crypto/md5"
5
+ "fmt"
6
+ "math/rand"
7
+ "net/url"
8
+ "time"
9
+ )
10
+
11
+ func SignURL(originURL, privateKey string, uid uint64, validDuration time.Duration) (newURL string, err error) {
12
+ if privateKey == "" {
13
+ return originURL, nil
14
+ }
15
+ var (
16
+ ts = time.Now().Add(validDuration).Unix() // 有效时间戳
17
+ rInt = rand.Int() // 随机正整数
18
+ objURL *url.URL
19
+ )
20
+ objURL, err = url.Parse(originURL)
21
+ if err != nil {
22
+ return "", err
23
+ }
24
+ authKey := fmt.Sprintf("%d-%d-%d-%x", ts, rInt, uid, md5.Sum([]byte(fmt.Sprintf("%s-%d-%d-%d-%s",
25
+ objURL.Path, ts, rInt, uid, privateKey))))
26
+ v := objURL.Query()
27
+ v.Add("auth_key", authKey)
28
+ objURL.RawQuery = v.Encode()
29
+ return objURL.String(), nil
30
+ }
drivers/123_share/driver.go ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "fmt"
7
+ "golang.org/x/time/rate"
8
+ "net/http"
9
+ "net/url"
10
+ "sync"
11
+ "time"
12
+
13
+ "github.com/alist-org/alist/v3/drivers/base"
14
+ "github.com/alist-org/alist/v3/internal/driver"
15
+ "github.com/alist-org/alist/v3/internal/errs"
16
+ "github.com/alist-org/alist/v3/internal/model"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ log "github.com/sirupsen/logrus"
20
+ )
21
+
22
+ type Pan123Share struct {
23
+ model.Storage
24
+ Addition
25
+ apiRateLimit sync.Map
26
+ }
27
+
28
+ func (d *Pan123Share) Config() driver.Config {
29
+ return config
30
+ }
31
+
32
+ func (d *Pan123Share) GetAddition() driver.Additional {
33
+ return &d.Addition
34
+ }
35
+
36
+ func (d *Pan123Share) Init(ctx context.Context) error {
37
+ // TODO login / refresh token
38
+ //op.MustSaveDriverStorage(d)
39
+ return nil
40
+ }
41
+
42
+ func (d *Pan123Share) Drop(ctx context.Context) error {
43
+ return nil
44
+ }
45
+
46
+ func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
47
+ // TODO return the files list, required
48
+ files, err := d.getFiles(ctx, dir.GetID())
49
+ if err != nil {
50
+ return nil, err
51
+ }
52
+ return utils.SliceConvert(files, func(src File) (model.Obj, error) {
53
+ return src, nil
54
+ })
55
+ }
56
+
57
+ func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
58
+ // TODO return link of file, required
59
+ if f, ok := file.(File); ok {
60
+ //var resp DownResp
61
+ var headers map[string]string
62
+ if !utils.IsLocalIPAddr(args.IP) {
63
+ headers = map[string]string{
64
+ //"X-Real-IP": "1.1.1.1",
65
+ "X-Forwarded-For": args.IP,
66
+ }
67
+ }
68
+ data := base.Json{
69
+ "shareKey": d.ShareKey,
70
+ "SharePwd": d.SharePwd,
71
+ "etag": f.Etag,
72
+ "fileId": f.FileId,
73
+ "s3keyFlag": f.S3KeyFlag,
74
+ "size": f.Size,
75
+ }
76
+ resp, err := d.request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
77
+ req.SetBody(data).SetHeaders(headers)
78
+ }, nil)
79
+ if err != nil {
80
+ return nil, err
81
+ }
82
+ downloadUrl := utils.Json.Get(resp, "data", "DownloadURL").ToString()
83
+ u, err := url.Parse(downloadUrl)
84
+ if err != nil {
85
+ return nil, err
86
+ }
87
+ nu := u.Query().Get("params")
88
+ if nu != "" {
89
+ du, _ := base64.StdEncoding.DecodeString(nu)
90
+ u, err = url.Parse(string(du))
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+ }
95
+ u_ := u.String()
96
+ log.Debug("download url: ", u_)
97
+ res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
98
+ if err != nil {
99
+ return nil, err
100
+ }
101
+ log.Debug(res.String())
102
+ link := model.Link{
103
+ URL: u_,
104
+ }
105
+ log.Debugln("res code: ", res.StatusCode())
106
+ if res.StatusCode() == 302 {
107
+ link.URL = res.Header().Get("location")
108
+ } else if res.StatusCode() < 300 {
109
+ link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
110
+ }
111
+ link.Header = http.Header{
112
+ "Referer": []string{"https://www.123pan.com/"},
113
+ }
114
+ return &link, nil
115
+ }
116
+ return nil, fmt.Errorf("can't convert obj")
117
+ }
118
+
119
+ func (d *Pan123Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
120
+ // TODO create folder, optional
121
+ return errs.NotSupport
122
+ }
123
+
124
+ func (d *Pan123Share) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
125
+ // TODO move obj, optional
126
+ return errs.NotSupport
127
+ }
128
+
129
+ func (d *Pan123Share) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
130
+ // TODO rename obj, optional
131
+ return errs.NotSupport
132
+ }
133
+
134
+ func (d *Pan123Share) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
135
+ // TODO copy obj, optional
136
+ return errs.NotSupport
137
+ }
138
+
139
+ func (d *Pan123Share) Remove(ctx context.Context, obj model.Obj) error {
140
+ // TODO remove obj, optional
141
+ return errs.NotSupport
142
+ }
143
+
144
+ func (d *Pan123Share) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
145
+ // TODO upload file, optional
146
+ return errs.NotSupport
147
+ }
148
+
149
+ //func (d *Pan123Share) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
150
+ // return nil, errs.NotSupport
151
+ //}
152
+
153
+ func (d *Pan123Share) APIRateLimit(ctx context.Context, api string) error {
154
+ value, _ := d.apiRateLimit.LoadOrStore(api,
155
+ rate.NewLimiter(rate.Every(700*time.Millisecond), 1))
156
+ limiter := value.(*rate.Limiter)
157
+
158
+ return limiter.Wait(ctx)
159
+ }
160
+
161
+ var _ driver.Driver = (*Pan123Share)(nil)
drivers/123_share/meta.go ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/internal/driver"
5
+ "github.com/alist-org/alist/v3/internal/op"
6
+ )
7
+
8
+ type Addition struct {
9
+ ShareKey string `json:"sharekey" required:"true"`
10
+ SharePwd string `json:"sharepassword"`
11
+ driver.RootID
12
+ //OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
13
+ //OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
14
+ AccessToken string `json:"accesstoken" type:"text"`
15
+ }
16
+
17
+ var config = driver.Config{
18
+ Name: "123PanShare",
19
+ LocalSort: true,
20
+ OnlyLocal: false,
21
+ OnlyProxy: false,
22
+ NoCache: false,
23
+ NoUpload: true,
24
+ NeedMs: false,
25
+ DefaultRoot: "0",
26
+ CheckStatus: false,
27
+ Alert: "",
28
+ NoOverwriteUpload: false,
29
+ }
30
+
31
+ func init() {
32
+ op.RegisterDriver(func() driver.Driver {
33
+ return &Pan123Share{}
34
+ })
35
+ }
drivers/123_share/types.go ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "github.com/alist-org/alist/v3/pkg/utils"
5
+ "net/url"
6
+ "path"
7
+ "strconv"
8
+ "strings"
9
+ "time"
10
+
11
+ "github.com/alist-org/alist/v3/internal/model"
12
+ )
13
+
14
+ type File struct {
15
+ FileName string `json:"FileName"`
16
+ Size int64 `json:"Size"`
17
+ UpdateAt time.Time `json:"UpdateAt"`
18
+ FileId int64 `json:"FileId"`
19
+ Type int `json:"Type"`
20
+ Etag string `json:"Etag"`
21
+ S3KeyFlag string `json:"S3KeyFlag"`
22
+ DownloadUrl string `json:"DownloadUrl"`
23
+ }
24
+
25
+ func (f File) GetHash() utils.HashInfo {
26
+ return utils.HashInfo{}
27
+ }
28
+
29
+ func (f File) GetPath() string {
30
+ return ""
31
+ }
32
+
33
+ func (f File) GetSize() int64 {
34
+ return f.Size
35
+ }
36
+
37
+ func (f File) GetName() string {
38
+ return f.FileName
39
+ }
40
+
41
+ func (f File) ModTime() time.Time {
42
+ return f.UpdateAt
43
+ }
44
+ func (f File) CreateTime() time.Time {
45
+ return f.UpdateAt
46
+ }
47
+
48
+ func (f File) IsDir() bool {
49
+ return f.Type == 1
50
+ }
51
+
52
+ func (f File) GetID() string {
53
+ return strconv.FormatInt(f.FileId, 10)
54
+ }
55
+
56
+ func (f File) Thumb() string {
57
+ if f.DownloadUrl == "" {
58
+ return ""
59
+ }
60
+ du, err := url.Parse(f.DownloadUrl)
61
+ if err != nil {
62
+ return ""
63
+ }
64
+ du.Path = strings.TrimSuffix(du.Path, "_24_24") + "_70_70"
65
+ query := du.Query()
66
+ query.Set("w", "70")
67
+ query.Set("h", "70")
68
+ if !query.Has("type") {
69
+ query.Set("type", strings.TrimPrefix(path.Base(f.FileName), "."))
70
+ }
71
+ if !query.Has("trade_key") {
72
+ query.Set("trade_key", "123pan-thumbnail")
73
+ }
74
+ du.RawQuery = query.Encode()
75
+ return du.String()
76
+ }
77
+
78
+ var _ model.Obj = (*File)(nil)
79
+ var _ model.Thumb = (*File)(nil)
80
+
81
+ //func (f File) Thumb() string {
82
+ //
83
+ //}
84
+ //var _ model.Thumb = (*File)(nil)
85
+
86
+ type Files struct {
87
+ //BaseResp
88
+ Data struct {
89
+ InfoList []File `json:"InfoList"`
90
+ Next string `json:"Next"`
91
+ } `json:"data"`
92
+ }
93
+
94
+ //type DownResp struct {
95
+ // //BaseResp
96
+ // Data struct {
97
+ // DownloadUrl string `json:"DownloadUrl"`
98
+ // } `json:"data"`
99
+ //}
drivers/123_share/util.go ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123Share
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "hash/crc32"
8
+ "math"
9
+ "math/rand"
10
+ "net/http"
11
+ "net/url"
12
+ "strconv"
13
+ "strings"
14
+ "time"
15
+
16
+ "github.com/alist-org/alist/v3/drivers/base"
17
+ "github.com/alist-org/alist/v3/pkg/utils"
18
+ "github.com/go-resty/resty/v2"
19
+ jsoniter "github.com/json-iterator/go"
20
+ )
21
+
22
+ const (
23
+ Api = "https://www.123pan.com/api"
24
+ AApi = "https://www.123pan.com/a/api"
25
+ BApi = "https://www.123pan.com/b/api"
26
+ MainApi = BApi
27
+ FileList = MainApi + "/share/get"
28
+ DownloadInfo = MainApi + "/share/download/info"
29
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
30
+ )
31
+
32
+ func signPath(path string, os string, version string) (k string, v string) {
33
+ table := []byte{'a', 'd', 'e', 'f', 'g', 'h', 'l', 'm', 'y', 'i', 'j', 'n', 'o', 'p', 'k', 'q', 'r', 's', 't', 'u', 'b', 'c', 'v', 'w', 's', 'z'}
34
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
35
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
36
+ timestamp := fmt.Sprint(now.Unix())
37
+ nowStr := []byte(now.Format("200601021504"))
38
+ for i := 0; i < len(nowStr); i++ {
39
+ nowStr[i] = table[nowStr[i]-48]
40
+ }
41
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
42
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
43
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
44
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
45
+ }
46
+
47
+ func GetApi(rawUrl string) string {
48
+ u, _ := url.Parse(rawUrl)
49
+ query := u.Query()
50
+ query.Add(signPath(u.Path, "web", "3"))
51
+ u.RawQuery = query.Encode()
52
+ return u.String()
53
+ }
54
+
55
+ func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
56
+ req := base.RestyClient.R()
57
+ req.SetHeaders(map[string]string{
58
+ "origin": "https://www.123pan.com",
59
+ "referer": "https://www.123pan.com/",
60
+ "authorization": "Bearer " + d.AccessToken,
61
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
62
+ "platform": "web",
63
+ "app-version": "3",
64
+ //"user-agent": base.UserAgent,
65
+ })
66
+ if callback != nil {
67
+ callback(req)
68
+ }
69
+ if resp != nil {
70
+ req.SetResult(resp)
71
+ }
72
+ res, err := req.Execute(method, GetApi(url))
73
+ if err != nil {
74
+ return nil, err
75
+ }
76
+ body := res.Body()
77
+ code := utils.Json.Get(body, "code").ToInt()
78
+ if code != 0 {
79
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
80
+ }
81
+ return body, nil
82
+ }
83
+
84
+ func (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {
85
+ page := 1
86
+ res := make([]File, 0)
87
+ for {
88
+ if err := d.APIRateLimit(ctx, FileList); err != nil {
89
+ return nil, err
90
+ }
91
+ var resp Files
92
+ query := map[string]string{
93
+ "limit": "100",
94
+ "next": "0",
95
+ "orderBy": "file_id",
96
+ "orderDirection": "desc",
97
+ "parentFileId": parentId,
98
+ "Page": strconv.Itoa(page),
99
+ "shareKey": d.ShareKey,
100
+ "SharePwd": d.SharePwd,
101
+ }
102
+ _, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
103
+ req.SetQueryParams(query)
104
+ }, &resp)
105
+ if err != nil {
106
+ return nil, err
107
+ }
108
+ page++
109
+ res = append(res, resp.Data.InfoList...)
110
+ if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
111
+ break
112
+ }
113
+ }
114
+ return res, nil
115
+ }
116
+
117
+ // do others that not defined in Driver interface
drivers/139/driver.go ADDED
@@ -0,0 +1,653 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "context"
5
+ "encoding/base64"
6
+ "fmt"
7
+ "io"
8
+ "net/http"
9
+ "strconv"
10
+ "strings"
11
+ "time"
12
+
13
+ "github.com/alist-org/alist/v3/drivers/base"
14
+ "github.com/alist-org/alist/v3/internal/driver"
15
+ "github.com/alist-org/alist/v3/internal/errs"
16
+ "github.com/alist-org/alist/v3/internal/model"
17
+ "github.com/alist-org/alist/v3/pkg/cron"
18
+ "github.com/alist-org/alist/v3/pkg/utils"
19
+ "github.com/google/uuid"
20
+ log "github.com/sirupsen/logrus"
21
+ )
22
+
23
+ type Yun139 struct {
24
+ model.Storage
25
+ Addition
26
+ cron *cron.Cron
27
+ Account string
28
+ }
29
+
30
+ func (d *Yun139) Config() driver.Config {
31
+ return config
32
+ }
33
+
34
+ func (d *Yun139) GetAddition() driver.Additional {
35
+ return &d.Addition
36
+ }
37
+
38
+ func (d *Yun139) Init(ctx context.Context) error {
39
+ if d.Authorization == "" {
40
+ return fmt.Errorf("authorization is empty")
41
+ }
42
+ d.cron = cron.NewCron(time.Hour * 24 * 7)
43
+ d.cron.Do(func() {
44
+ err := d.refreshToken()
45
+ if err != nil {
46
+ log.Errorf("%+v", err)
47
+ }
48
+ })
49
+ switch d.Addition.Type {
50
+ case MetaPersonalNew:
51
+ if len(d.Addition.RootFolderID) == 0 {
52
+ d.RootFolderID = "/"
53
+ }
54
+ return nil
55
+ case MetaPersonal:
56
+ if len(d.Addition.RootFolderID) == 0 {
57
+ d.RootFolderID = "root"
58
+ }
59
+ fallthrough
60
+ case MetaFamily:
61
+ decode, err := base64.StdEncoding.DecodeString(d.Authorization)
62
+ if err != nil {
63
+ return err
64
+ }
65
+ decodeStr := string(decode)
66
+ splits := strings.Split(decodeStr, ":")
67
+ if len(splits) < 2 {
68
+ return fmt.Errorf("authorization is invalid, splits < 2")
69
+ }
70
+ d.Account = splits[1]
71
+ _, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
72
+ "qryUserExternInfoReq": base.Json{
73
+ "commonAccountInfo": base.Json{
74
+ "account": d.Account,
75
+ "accountType": 1,
76
+ },
77
+ },
78
+ }, nil)
79
+ return err
80
+ default:
81
+ return errs.NotImplement
82
+ }
83
+ }
84
+
85
+ func (d *Yun139) Drop(ctx context.Context) error {
86
+ if d.cron != nil {
87
+ d.cron.Stop()
88
+ }
89
+ return nil
90
+ }
91
+
92
+ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
93
+ switch d.Addition.Type {
94
+ case MetaPersonalNew:
95
+ return d.personalGetFiles(dir.GetID())
96
+ case MetaPersonal:
97
+ return d.getFiles(dir.GetID())
98
+ case MetaFamily:
99
+ return d.familyGetFiles(dir.GetID())
100
+ default:
101
+ return nil, errs.NotImplement
102
+ }
103
+ }
104
+
105
+ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
106
+ var url string
107
+ var err error
108
+ switch d.Addition.Type {
109
+ case MetaPersonalNew:
110
+ url, err = d.personalGetLink(file.GetID())
111
+ case MetaPersonal:
112
+ fallthrough
113
+ case MetaFamily:
114
+ url, err = d.getLink(file.GetID())
115
+ default:
116
+ return nil, errs.NotImplement
117
+ }
118
+ if err != nil {
119
+ return nil, err
120
+ }
121
+ return &model.Link{URL: url}, nil
122
+ }
123
+
124
+ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
125
+ var err error
126
+ switch d.Addition.Type {
127
+ case MetaPersonalNew:
128
+ data := base.Json{
129
+ "parentFileId": parentDir.GetID(),
130
+ "name": dirName,
131
+ "description": "",
132
+ "type": "folder",
133
+ "fileRenameMode": "force_rename",
134
+ }
135
+ pathname := "/hcy/file/create"
136
+ _, err = d.personalPost(pathname, data, nil)
137
+ case MetaPersonal:
138
+ data := base.Json{
139
+ "createCatalogExtReq": base.Json{
140
+ "parentCatalogID": parentDir.GetID(),
141
+ "newCatalogName": dirName,
142
+ "commonAccountInfo": base.Json{
143
+ "account": d.Account,
144
+ "accountType": 1,
145
+ },
146
+ },
147
+ }
148
+ pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
149
+ _, err = d.post(pathname, data, nil)
150
+ case MetaFamily:
151
+ cataID := parentDir.GetID()
152
+ path := cataID
153
+ data := base.Json{
154
+ "cloudID": d.CloudID,
155
+ "commonAccountInfo": base.Json{
156
+ "account": d.Account,
157
+ "accountType": 1,
158
+ },
159
+ "docLibName": dirName,
160
+ "path": path,
161
+ }
162
+ pathname := "/orchestration/familyCloud-rebuild/cloudCatalog/v1.0/createCloudDoc"
163
+ _, err = d.post(pathname, data, nil)
164
+ default:
165
+ err = errs.NotImplement
166
+ }
167
+ return err
168
+ }
169
+
170
+ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
171
+ switch d.Addition.Type {
172
+ case MetaPersonalNew:
173
+ data := base.Json{
174
+ "fileIds": []string{srcObj.GetID()},
175
+ "toParentFileId": dstDir.GetID(),
176
+ }
177
+ pathname := "/hcy/file/batchMove"
178
+ _, err := d.personalPost(pathname, data, nil)
179
+ if err != nil {
180
+ return nil, err
181
+ }
182
+ return srcObj, nil
183
+ case MetaPersonal:
184
+ var contentInfoList []string
185
+ var catalogInfoList []string
186
+ if srcObj.IsDir() {
187
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
188
+ } else {
189
+ contentInfoList = append(contentInfoList, srcObj.GetID())
190
+ }
191
+ data := base.Json{
192
+ "createBatchOprTaskReq": base.Json{
193
+ "taskType": 3,
194
+ "actionType": "304",
195
+ "taskInfo": base.Json{
196
+ "contentInfoList": contentInfoList,
197
+ "catalogInfoList": catalogInfoList,
198
+ "newCatalogID": dstDir.GetID(),
199
+ },
200
+ "commonAccountInfo": base.Json{
201
+ "account": d.Account,
202
+ "accountType": 1,
203
+ },
204
+ },
205
+ }
206
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
207
+ _, err := d.post(pathname, data, nil)
208
+ if err != nil {
209
+ return nil, err
210
+ }
211
+ return srcObj, nil
212
+ default:
213
+ return nil, errs.NotImplement
214
+ }
215
+ }
216
+
217
+ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
218
+ var err error
219
+ switch d.Addition.Type {
220
+ case MetaPersonalNew:
221
+ data := base.Json{
222
+ "fileId": srcObj.GetID(),
223
+ "name": newName,
224
+ "description": "",
225
+ }
226
+ pathname := "/hcy/file/update"
227
+ _, err = d.personalPost(pathname, data, nil)
228
+ case MetaPersonal:
229
+ var data base.Json
230
+ var pathname string
231
+ if srcObj.IsDir() {
232
+ data = base.Json{
233
+ "catalogID": srcObj.GetID(),
234
+ "catalogName": newName,
235
+ "commonAccountInfo": base.Json{
236
+ "account": d.Account,
237
+ "accountType": 1,
238
+ },
239
+ }
240
+ pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
241
+ } else {
242
+ data = base.Json{
243
+ "contentID": srcObj.GetID(),
244
+ "contentName": newName,
245
+ "commonAccountInfo": base.Json{
246
+ "account": d.Account,
247
+ "accountType": 1,
248
+ },
249
+ }
250
+ pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
251
+ }
252
+ _, err = d.post(pathname, data, nil)
253
+ default:
254
+ err = errs.NotImplement
255
+ }
256
+ return err
257
+ }
258
+
259
+ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
260
+ var err error
261
+ switch d.Addition.Type {
262
+ case MetaPersonalNew:
263
+ data := base.Json{
264
+ "fileIds": []string{srcObj.GetID()},
265
+ "toParentFileId": dstDir.GetID(),
266
+ }
267
+ pathname := "/hcy/file/batchCopy"
268
+ _, err := d.personalPost(pathname, data, nil)
269
+ return err
270
+ case MetaPersonal:
271
+ var contentInfoList []string
272
+ var catalogInfoList []string
273
+ if srcObj.IsDir() {
274
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
275
+ } else {
276
+ contentInfoList = append(contentInfoList, srcObj.GetID())
277
+ }
278
+ data := base.Json{
279
+ "createBatchOprTaskReq": base.Json{
280
+ "taskType": 3,
281
+ "actionType": 309,
282
+ "taskInfo": base.Json{
283
+ "contentInfoList": contentInfoList,
284
+ "catalogInfoList": catalogInfoList,
285
+ "newCatalogID": dstDir.GetID(),
286
+ },
287
+ "commonAccountInfo": base.Json{
288
+ "account": d.Account,
289
+ "accountType": 1,
290
+ },
291
+ },
292
+ }
293
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
294
+ _, err = d.post(pathname, data, nil)
295
+ default:
296
+ err = errs.NotImplement
297
+ }
298
+ return err
299
+ }
300
+
301
+ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
302
+ switch d.Addition.Type {
303
+ case MetaPersonalNew:
304
+ data := base.Json{
305
+ "fileIds": []string{obj.GetID()},
306
+ }
307
+ pathname := "/hcy/recyclebin/batchTrash"
308
+ _, err := d.personalPost(pathname, data, nil)
309
+ return err
310
+ case MetaPersonal:
311
+ fallthrough
312
+ case MetaFamily:
313
+ return errs.NotImplement
314
+ log.Warn("==========================================")
315
+ var contentInfoList []string
316
+ var catalogInfoList []string
317
+ cataID := obj.GetID()
318
+ path := ""
319
+ if strings.Contains(cataID, "/") {
320
+ lastSlashIndex := strings.LastIndex(cataID, "/")
321
+ path = cataID[0:lastSlashIndex]
322
+ cataID = cataID[lastSlashIndex+1:]
323
+ }
324
+
325
+ if obj.IsDir() {
326
+ catalogInfoList = append(catalogInfoList, cataID)
327
+ } else {
328
+ contentInfoList = append(contentInfoList, cataID)
329
+ }
330
+ data := base.Json{
331
+ "createBatchOprTaskReq": base.Json{
332
+ "taskType": 2,
333
+ "actionType": 201,
334
+ "taskInfo": base.Json{
335
+ "newCatalogID": "",
336
+ "contentInfoList": contentInfoList,
337
+ "catalogInfoList": catalogInfoList,
338
+ },
339
+ "commonAccountInfo": base.Json{
340
+ "account": d.Account,
341
+ "accountType": 1,
342
+ },
343
+ },
344
+ }
345
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
346
+ if d.isFamily() {
347
+ data = base.Json{
348
+ "taskType": 2,
349
+ "sourceCloudID": d.CloudID,
350
+ "sourceCatalogType": 1002,
351
+ "path": path,
352
+ "contentList": catalogInfoList,
353
+ "catalogList": contentInfoList,
354
+ "commonAccountInfo": base.Json{
355
+ "account": d.Account,
356
+ "accountType": 1,
357
+ },
358
+ }
359
+ pathname = "/orchestration/familyCloud-rebuild/batchOprTask/v1.0/createBatchOprTask"
360
+ }
361
+ _, err := d.post(pathname, data, nil)
362
+ return err
363
+ default:
364
+ return errs.NotImplement
365
+ }
366
+ }
367
+
368
+ const (
369
+ _ = iota //ignore first value by assigning to blank identifier
370
+ KB = 1 << (10 * iota)
371
+ MB
372
+ GB
373
+ TB
374
+ )
375
+
376
+ func getPartSize(size int64) int64 {
377
+ // 网盘对于分片数量存在上限
378
+ if size/GB > 30 {
379
+ return 512 * MB
380
+ }
381
+ return 350 * MB
382
+ }
383
+
384
+
385
+
386
+ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
387
+ switch d.Addition.Type {
388
+ case MetaPersonalNew:
389
+ var err error
390
+ fullHash := stream.GetHash().GetHash(utils.SHA256)
391
+ if len(fullHash) <= 0 {
392
+ tmpF, err := stream.CacheFullInTempFile()
393
+ if err != nil {
394
+ return err
395
+ }
396
+ fullHash, err = utils.HashFile(utils.SHA256, tmpF)
397
+ if err != nil {
398
+ return err
399
+ }
400
+ }
401
+
402
+ partInfos := []PartInfo{}
403
+ var partSize = getPartSize(stream.GetSize())
404
+ part := (stream.GetSize() + partSize - 1) / partSize
405
+ if part == 0 {
406
+ part = 1
407
+ }
408
+ for i := int64(0); i < part; i++ {
409
+ if utils.IsCanceled(ctx) {
410
+ return ctx.Err()
411
+ }
412
+
413
+ start := i * partSize
414
+ byteSize := stream.GetSize() - start
415
+ if byteSize > partSize {
416
+ byteSize = partSize
417
+ }
418
+ partNumber := i + 1
419
+ partInfo := PartInfo{
420
+ PartNumber: partNumber,
421
+ PartSize: byteSize,
422
+ ParallelHashCtx: ParallelHashCtx{
423
+ PartOffset: start,
424
+ },
425
+ }
426
+ partInfos = append(partInfos, partInfo)
427
+ }
428
+
429
+ // return errs.NotImplement
430
+ data := base.Json{
431
+ "contentHash": fullHash,
432
+ "contentHashAlgorithm": "SHA256",
433
+ "contentType": "application/octet-stream",
434
+ "parallelUpload": false,
435
+ "partInfos": partInfos,
436
+ "size": stream.GetSize(),
437
+ "parentFileId": dstDir.GetID(),
438
+ "name": stream.GetName(),
439
+ "type": "file",
440
+ "fileRenameMode": "auto_rename",
441
+ }
442
+ pathname := "/hcy/file/create"
443
+ var resp PersonalUploadResp
444
+ _, err = d.personalPost(pathname, data, &resp)
445
+ if err != nil {
446
+ return err
447
+ }
448
+
449
+ if resp.Data.Exist || resp.Data.RapidUpload {
450
+ return nil
451
+ }
452
+
453
+ // Progress
454
+ p := driver.NewProgress(stream.GetSize(), up)
455
+
456
+ // Update Progress
457
+ // r := io.TeeReader(stream, p)
458
+
459
+ for index, partInfo := range resp.Data.PartInfos {
460
+
461
+ int64Index := int64(index)
462
+ start := int64Index * partSize
463
+ byteSize := stream.GetSize() - start
464
+ if byteSize > partSize {
465
+ byteSize = partSize
466
+ }
467
+
468
+ retry := 2 // 只允许重试 2 次
469
+ for attempt := 0; attempt <= retry; attempt++ {
470
+ limitReader := io.LimitReader(stream, byteSize)
471
+ // Update Progress
472
+ r := io.TeeReader(limitReader, p)
473
+ req, err := http.NewRequest("PUT", partInfo.UploadUrl, r)
474
+ if err != nil {
475
+ return err
476
+ }
477
+ req = req.WithContext(ctx)
478
+ req.Header.Set("Content-Type", "application/octet-stream")
479
+ req.Header.Set("Content-Length", fmt.Sprint(byteSize))
480
+ req.Header.Set("Origin", "https://yun.139.com")
481
+ req.Header.Set("Referer", "https://yun.139.com/")
482
+ req.ContentLength = byteSize
483
+
484
+ res, err := base.HttpClient.Do(req)
485
+ if err != nil {
486
+ return err
487
+ }
488
+
489
+ _ = res.Body.Close()
490
+ log.Debugf("%+v", res)
491
+ if res.StatusCode != http.StatusOK {
492
+ if res.StatusCode == http.StatusRequestTimeout && attempt < retry{
493
+ log.Warn("服务器返回 408,尝试重试...")
494
+ continue
495
+ }else{
496
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
497
+ }
498
+ }
499
+ break
500
+ }
501
+ }
502
+
503
+ data = base.Json{
504
+ "contentHash": fullHash,
505
+ "contentHashAlgorithm": "SHA256",
506
+ "fileId": resp.Data.FileId,
507
+ "uploadId": resp.Data.UploadId,
508
+ }
509
+ _, err = d.personalPost("/hcy/file/complete", data, nil)
510
+ if err != nil {
511
+ return err
512
+ }
513
+ return nil
514
+ case MetaPersonal:
515
+ fallthrough
516
+ case MetaFamily:
517
+ data := base.Json{
518
+ "manualRename": 2,
519
+ "operation": 0,
520
+ "fileCount": 1,
521
+ "totalSize": 0, // 去除上传大小限制
522
+ "uploadContentList": []base.Json{{
523
+ "contentName": stream.GetName(),
524
+ "contentSize": stream.GetSize(), // 去除上传大小限制
525
+ // "digest": "5a3231986ce7a6b46e408612d385bafa"
526
+ }},
527
+ "parentCatalogID": dstDir.GetID(),
528
+ "newCatalogName": "",
529
+ "commonAccountInfo": base.Json{
530
+ "account": d.Account,
531
+ "accountType": 1,
532
+ },
533
+ }
534
+ pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
535
+ if d.isFamily() {
536
+ cataID := dstDir.GetID()
537
+ path := cataID
538
+ seqNo, _ := uuid.NewUUID()
539
+ data = base.Json{
540
+ "cloudID": d.CloudID,
541
+ "path": path,
542
+ "operation": 0,
543
+ "cloudType": 1,
544
+ "catalogType": 3,
545
+ "manualRename": 2,
546
+ "fileCount": 1,
547
+ "totalSize": stream.GetSize(),
548
+ "uploadContentList": []base.Json{{
549
+ "contentName": stream.GetName(),
550
+ "contentSize": stream.GetSize(),
551
+ }},
552
+ "seqNo": seqNo,
553
+ "commonAccountInfo": base.Json{
554
+ "account": d.Account,
555
+ "accountType": 1,
556
+ },
557
+ }
558
+ pathname = "/orchestration/familyCloud-rebuild/content/v1.0/getFileUploadURL"
559
+ //return errs.NotImplement
560
+ }
561
+ var resp UploadResp
562
+ _, err := d.post(pathname, data, &resp)
563
+ if err != nil {
564
+ return err
565
+ }
566
+ // Progress
567
+ p := driver.NewProgress(stream.GetSize(), up)
568
+
569
+ var partSize = getPartSize(stream.GetSize())
570
+ //var partSize = stream.GetSize()
571
+ part := (stream.GetSize() + partSize - 1) / partSize
572
+ if part == 0 {
573
+ part = 1
574
+ }
575
+ for i := int64(0); i < part; i++ {
576
+ if utils.IsCanceled(ctx) {
577
+ return ctx.Err()
578
+ }
579
+
580
+ start := i * partSize
581
+ byteSize := stream.GetSize() - start
582
+ if byteSize > partSize {
583
+ byteSize = partSize
584
+ }
585
+
586
+ retry := 2 // 只允许重试 2次
587
+ for attempt := 0; attempt <= retry; attempt++ {
588
+ limitReader := io.LimitReader(stream, byteSize)
589
+ // Update Progress
590
+ r := io.TeeReader(limitReader, p)
591
+ req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
592
+ if err != nil {
593
+ return err
594
+ }
595
+
596
+ req = req.WithContext(ctx)
597
+ req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
598
+ req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
599
+ req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
600
+ req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
601
+ req.Header.Set("rangeType", "0")
602
+ req.ContentLength = byteSize
603
+
604
+ res, err := base.HttpClient.Do(req)
605
+ if err != nil {
606
+ return err
607
+ }
608
+ _ = res.Body.Close()
609
+ log.Debugf("%+v", res)
610
+ if res.StatusCode != http.StatusOK {
611
+ if res.StatusCode == http.StatusRequestTimeout && attempt < retry {
612
+ log.Warn("服务器返回 408,尝试重试...")
613
+ continue
614
+ }else{
615
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
616
+ }
617
+ }
618
+ break
619
+ }
620
+ }
621
+
622
+ return nil
623
+ default:
624
+ return errs.NotImplement
625
+ }
626
+ }
627
+
628
+ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
629
+ switch d.Addition.Type {
630
+ case MetaPersonalNew:
631
+ var resp base.Json
632
+ var uri string
633
+ data := base.Json{
634
+ "category": "video",
635
+ "fileId": args.Obj.GetID(),
636
+ }
637
+ switch args.Method {
638
+ case "video_preview":
639
+ uri = "/hcy/videoPreview/getPreviewInfo"
640
+ default:
641
+ return nil, errs.NotSupport
642
+ }
643
+ _, err := d.personalPost(uri, data, &resp)
644
+ if err != nil {
645
+ return nil, err
646
+ }
647
+ return resp["data"], nil
648
+ default:
649
+ return nil, errs.NotImplement
650
+ }
651
+ }
652
+
653
+ var _ driver.Driver = (*Yun139)(nil)