hanfish commited on
Commit
215df2f
1 Parent(s): cb27eac
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
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,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM alpine:edge as builder
2
+ LABEL stage=go-builder
3
+ WORKDIR /app/
4
+ RUN apk add --no-cache bash curl gcc git go musl-dev
5
+ COPY go.mod go.sum ./
6
+ RUN go mod download
7
+ COPY ./ ./
8
+ RUN bash build.sh release docker
9
+
10
+ FROM alpine:edge
11
+ LABEL MAINTAINER="i@nn.ci"
12
+ VOLUME /opt/alist/data/
13
+ WORKDIR /opt/alist/
14
+ COPY --from=builder /app/bin/alist ./
15
+ COPY entrypoint.sh /entrypoint.sh
16
+ RUN apk update && \
17
+ apk upgrade --no-cache && \
18
+ apk add --no-cache bash ca-certificates su-exec tzdata; \
19
+ chmod +x /entrypoint.sh && \
20
+ rm -rf /var/cache/apk/*
21
+ ENV PUID=0 PGID=0 UMASK=022
22
+ EXPOSE 5244 5245
23
+ CMD [ "/entrypoint.sh" ]
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 xhofe/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/>.
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/alist-org/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/alist-org/alist/v3/internal/conf.BuiltAt=$builtAt' \
21
+ -X 'github.com/alist-org/alist/v3/internal/conf.GoVersion=$goVersion' \
22
+ -X 'github.com/alist-org/alist/v3/internal/conf.GitAuthor=$gitAuthor' \
23
+ -X 'github.com/alist-org/alist/v3/internal/conf.GitCommit=$gitCommit' \
24
+ -X 'github.com/alist-org/alist/v3/internal/conf.Version=$version' \
25
+ -X 'github.com/alist-org/alist/v3/internal/conf.WebVersion=$webVersion' \
26
+ "
27
+
28
+ FetchWebDev() {
29
+ curl -L https://codeload.github.com/alist-org/web-dist/tar.gz/refs/heads/dev -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/alist-org/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
+ }
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/driver.go ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "context"
5
+ "strings"
6
+
7
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
8
+ "github.com/alist-org/alist/v3/internal/driver"
9
+ "github.com/alist-org/alist/v3/internal/model"
10
+ "github.com/alist-org/alist/v3/pkg/http_range"
11
+ "github.com/alist-org/alist/v3/pkg/utils"
12
+ "github.com/pkg/errors"
13
+ "golang.org/x/time/rate"
14
+ )
15
+
16
+ type Pan115 struct {
17
+ model.Storage
18
+ Addition
19
+ client *driver115.Pan115Client
20
+ limiter *rate.Limiter
21
+ }
22
+
23
+ func (d *Pan115) Config() driver.Config {
24
+ return config
25
+ }
26
+
27
+ func (d *Pan115) GetAddition() driver.Additional {
28
+ return &d.Addition
29
+ }
30
+
31
+ func (d *Pan115) Init(ctx context.Context) error {
32
+ if d.LimitRate > 0 {
33
+ d.limiter = rate.NewLimiter(rate.Limit(d.LimitRate), 1)
34
+ }
35
+ return d.login()
36
+ }
37
+
38
+ func (d *Pan115) WaitLimit(ctx context.Context) error {
39
+ if d.limiter != nil {
40
+ return d.limiter.Wait(ctx)
41
+ }
42
+ return nil
43
+ }
44
+
45
+ func (d *Pan115) Drop(ctx context.Context) error {
46
+ return nil
47
+ }
48
+
49
+ func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
50
+ if err := d.WaitLimit(ctx); err != nil {
51
+ return nil, err
52
+ }
53
+ files, err := d.getFiles(dir.GetID())
54
+ if err != nil && !errors.Is(err, driver115.ErrNotExist) {
55
+ return nil, err
56
+ }
57
+ return utils.SliceConvert(files, func(src FileObj) (model.Obj, error) {
58
+ return &src, nil
59
+ })
60
+ }
61
+
62
+ func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
63
+ if err := d.WaitLimit(ctx); err != nil {
64
+ return nil, err
65
+ }
66
+ var userAgent = args.Header.Get("User-Agent")
67
+ downloadInfo, err := d.
68
+ DownloadWithUA(file.(*FileObj).PickCode, userAgent)
69
+ if err != nil {
70
+ return nil, err
71
+ }
72
+ link := &model.Link{
73
+ URL: downloadInfo.Url.Url,
74
+ Header: downloadInfo.Header,
75
+ }
76
+ return link, nil
77
+ }
78
+
79
+ func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
80
+ if err := d.WaitLimit(ctx); err != nil {
81
+ return err
82
+ }
83
+ if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
84
+ return err
85
+ }
86
+ return nil
87
+ }
88
+
89
+ func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
90
+ if err := d.WaitLimit(ctx); err != nil {
91
+ return err
92
+ }
93
+ return d.client.Move(dstDir.GetID(), srcObj.GetID())
94
+ }
95
+
96
+ func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
97
+ if err := d.WaitLimit(ctx); err != nil {
98
+ return err
99
+ }
100
+ return d.client.Rename(srcObj.GetID(), newName)
101
+ }
102
+
103
+ func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
104
+ if err := d.WaitLimit(ctx); err != nil {
105
+ return err
106
+ }
107
+ return d.client.Copy(dstDir.GetID(), srcObj.GetID())
108
+ }
109
+
110
+ func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
111
+ if err := d.WaitLimit(ctx); err != nil {
112
+ return err
113
+ }
114
+ return d.client.Delete(obj.GetID())
115
+ }
116
+
117
+ func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
118
+ if err := d.WaitLimit(ctx); err != nil {
119
+ return err
120
+ }
121
+
122
+ var (
123
+ fastInfo *driver115.UploadInitResp
124
+ dirID = dstDir.GetID()
125
+ )
126
+
127
+ if ok, err := d.client.UploadAvailable(); err != nil || !ok {
128
+ return err
129
+ }
130
+ if stream.GetSize() > d.client.UploadMetaInfo.SizeLimit {
131
+ return driver115.ErrUploadTooLarge
132
+ }
133
+ //if digest, err = d.client.GetDigestResult(stream); err != nil {
134
+ // return err
135
+ //}
136
+
137
+ const PreHashSize int64 = 128 * utils.KB
138
+ hashSize := PreHashSize
139
+ if stream.GetSize() < PreHashSize {
140
+ hashSize = stream.GetSize()
141
+ }
142
+ reader, err := stream.RangeRead(http_range.Range{Start: 0, Length: hashSize})
143
+ if err != nil {
144
+ return err
145
+ }
146
+ preHash, err := utils.HashReader(utils.SHA1, reader)
147
+ if err != nil {
148
+ return err
149
+ }
150
+ preHash = strings.ToUpper(preHash)
151
+ fullHash := stream.GetHash().GetHash(utils.SHA1)
152
+ if len(fullHash) <= 0 {
153
+ tmpF, err := stream.CacheFullInTempFile()
154
+ if err != nil {
155
+ return err
156
+ }
157
+ fullHash, err = utils.HashFile(utils.SHA1, tmpF)
158
+ if err != nil {
159
+ return err
160
+ }
161
+ }
162
+ fullHash = strings.ToUpper(fullHash)
163
+
164
+ // rapid-upload
165
+ // note that 115 add timeout for rapid-upload,
166
+ // and "sig invalid" err is thrown even when the hash is correct after timeout.
167
+ if fastInfo, err = d.rapidUpload(stream.GetSize(), stream.GetName(), dirID, preHash, fullHash, stream); err != nil {
168
+ return err
169
+ }
170
+ if matched, err := fastInfo.Ok(); err != nil {
171
+ return err
172
+ } else if matched {
173
+ return nil
174
+ }
175
+
176
+ // 闪传失败,上传
177
+ if stream.GetSize() <= utils.KB { // 文件大小小于1KB,改用普通模式上传
178
+ return d.client.UploadByOSS(&fastInfo.UploadOSSParams, stream, dirID)
179
+ }
180
+ // 分片上传
181
+ return d.UploadByMultipart(&fastInfo.UploadOSSParams, stream.GetSize(), stream, dirID)
182
+
183
+ }
184
+
185
+ 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,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"56" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"number" default:"2" help:"limit all api request rate (1r/[limit_rate]s)"`
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,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "github.com/SheltonZhu/115driver/pkg/driver"
5
+ "github.com/alist-org/alist/v3/internal/model"
6
+ "github.com/alist-org/alist/v3/pkg/utils"
7
+ "time"
8
+ )
9
+
10
+ var _ model.Obj = (*FileObj)(nil)
11
+
12
+ type FileObj struct {
13
+ driver.File
14
+ }
15
+
16
+ func (f *FileObj) CreateTime() time.Time {
17
+ return f.File.CreateTime
18
+ }
19
+
20
+ func (f *FileObj) GetHash() utils.HashInfo {
21
+ return utils.NewHashInfo(utils.SHA1, f.Sha1)
22
+ }
drivers/115/util.go ADDED
@@ -0,0 +1,479 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _115
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/tls"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "net/http"
10
+ "net/url"
11
+ "path/filepath"
12
+ "strconv"
13
+ "strings"
14
+ "sync"
15
+ "time"
16
+
17
+ "github.com/alist-org/alist/v3/internal/conf"
18
+ "github.com/alist-org/alist/v3/internal/model"
19
+ "github.com/alist-org/alist/v3/pkg/http_range"
20
+ "github.com/alist-org/alist/v3/pkg/utils"
21
+ "github.com/aliyun/aliyun-oss-go-sdk/oss"
22
+
23
+ driver115 "github.com/SheltonZhu/115driver/pkg/driver"
24
+ crypto "github.com/gaoyb7/115drive-webdav/115"
25
+ "github.com/orzogc/fake115uploader/cipher"
26
+ "github.com/pkg/errors"
27
+ )
28
+
29
+ var UserAgent = driver115.UA115Desktop
30
+
31
+ func (d *Pan115) login() error {
32
+ var err error
33
+ opts := []driver115.Option{
34
+ driver115.UA(UserAgent),
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.Addition.QRCodeToken != "" {
42
+ s := &driver115.QRCodeSession{
43
+ UID: d.Addition.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.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
49
+ d.Addition.QRCodeToken = ""
50
+ } else if d.Addition.Cookie != "" {
51
+ if err = cr.FromCookie(d.Addition.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
+ const (
77
+ appVer = "2.0.3.6"
78
+ )
79
+
80
+ func (c *Pan115) DownloadWithUA(pickCode, ua string) (*driver115.DownloadInfo, error) {
81
+ key := crypto.GenerateKey()
82
+ result := driver115.DownloadResp{}
83
+ params, err := utils.Json.Marshal(map[string]string{"pickcode": pickCode})
84
+ if err != nil {
85
+ return nil, err
86
+ }
87
+
88
+ data := crypto.Encode(params, key)
89
+
90
+ bodyReader := strings.NewReader(url.Values{"data": []string{data}}.Encode())
91
+ reqUrl := fmt.Sprintf("%s?t=%s", driver115.ApiDownloadGetUrl, driver115.Now().String())
92
+ req, _ := http.NewRequest(http.MethodPost, reqUrl, bodyReader)
93
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
94
+ req.Header.Set("Cookie", c.Cookie)
95
+ req.Header.Set("User-Agent", ua)
96
+
97
+ resp, err := c.client.Client.GetClient().Do(req)
98
+ if err != nil {
99
+ return nil, err
100
+ }
101
+ defer resp.Body.Close()
102
+
103
+ body, err := io.ReadAll(resp.Body)
104
+ if err != nil {
105
+ return nil, err
106
+ }
107
+ if err := utils.Json.Unmarshal(body, &result); err != nil {
108
+ return nil, err
109
+ }
110
+
111
+ if err = result.Err(string(body)); err != nil {
112
+ return nil, err
113
+ }
114
+
115
+ bytes, err := crypto.Decode(string(result.EncodedData), key)
116
+ if err != nil {
117
+ return nil, err
118
+ }
119
+
120
+ downloadInfo := driver115.DownloadData{}
121
+ if err := utils.Json.Unmarshal(bytes, &downloadInfo); err != nil {
122
+ return nil, err
123
+ }
124
+
125
+ for _, info := range downloadInfo {
126
+ if info.FileSize < 0 {
127
+ return nil, driver115.ErrDownloadEmpty
128
+ }
129
+ info.Header = resp.Request.Header
130
+ return info, nil
131
+ }
132
+ return nil, driver115.ErrUnexpected
133
+ }
134
+
135
+ func (d *Pan115) rapidUpload(fileSize int64, fileName, dirID, preID, fileID string, stream model.FileStreamer) (*driver115.UploadInitResp, error) {
136
+ var (
137
+ ecdhCipher *cipher.EcdhCipher
138
+ encrypted []byte
139
+ decrypted []byte
140
+ encodedToken string
141
+ err error
142
+ target = "U_1_" + dirID
143
+ bodyBytes []byte
144
+ result = driver115.UploadInitResp{}
145
+ fileSizeStr = strconv.FormatInt(fileSize, 10)
146
+ )
147
+ if ecdhCipher, err = cipher.NewEcdhCipher(); err != nil {
148
+ return nil, err
149
+ }
150
+
151
+ userID := strconv.FormatInt(d.client.UserID, 10)
152
+ form := url.Values{}
153
+ form.Set("appid", "0")
154
+ form.Set("appversion", appVer)
155
+ form.Set("userid", userID)
156
+ form.Set("filename", fileName)
157
+ form.Set("filesize", fileSizeStr)
158
+ form.Set("fileid", fileID)
159
+ form.Set("target", target)
160
+ form.Set("sig", d.client.GenerateSignature(fileID, target))
161
+
162
+ signKey, signVal := "", ""
163
+ for retry := true; retry; {
164
+ t := driver115.Now()
165
+
166
+ if encodedToken, err = ecdhCipher.EncodeToken(t.ToInt64()); err != nil {
167
+ return nil, err
168
+ }
169
+
170
+ params := map[string]string{
171
+ "k_ec": encodedToken,
172
+ }
173
+
174
+ form.Set("t", t.String())
175
+ form.Set("token", d.client.GenerateToken(fileID, preID, t.String(), fileSizeStr, signKey, signVal))
176
+ if signKey != "" && signVal != "" {
177
+ form.Set("sign_key", signKey)
178
+ form.Set("sign_val", signVal)
179
+ }
180
+ if encrypted, err = ecdhCipher.Encrypt([]byte(form.Encode())); err != nil {
181
+ return nil, err
182
+ }
183
+
184
+ req := d.client.NewRequest().
185
+ SetQueryParams(params).
186
+ SetBody(encrypted).
187
+ SetHeaderVerbatim("Content-Type", "application/x-www-form-urlencoded").
188
+ SetDoNotParseResponse(true)
189
+ resp, err := req.Post(driver115.ApiUploadInit)
190
+ if err != nil {
191
+ return nil, err
192
+ }
193
+ data := resp.RawBody()
194
+ defer data.Close()
195
+ if bodyBytes, err = io.ReadAll(data); err != nil {
196
+ return nil, err
197
+ }
198
+ if decrypted, err = ecdhCipher.Decrypt(bodyBytes); err != nil {
199
+ return nil, err
200
+ }
201
+ if err = driver115.CheckErr(json.Unmarshal(decrypted, &result), &result, resp); err != nil {
202
+ return nil, err
203
+ }
204
+ if result.Status == 7 {
205
+ // Update signKey & signVal
206
+ signKey = result.SignKey
207
+ signVal, err = UploadDigestRange(stream, result.SignCheck)
208
+ if err != nil {
209
+ return nil, err
210
+ }
211
+ } else {
212
+ retry = false
213
+ }
214
+ result.SHA1 = fileID
215
+ }
216
+
217
+ return &result, nil
218
+ }
219
+
220
+ func UploadDigestRange(stream model.FileStreamer, rangeSpec string) (result string, err error) {
221
+ var start, end int64
222
+ if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil {
223
+ return
224
+ }
225
+
226
+ length := end - start + 1
227
+ reader, err := stream.RangeRead(http_range.Range{Start: start, Length: length})
228
+ hashStr, err := utils.HashReader(utils.SHA1, reader)
229
+ if err != nil {
230
+ return "", err
231
+ }
232
+ result = strings.ToUpper(hashStr)
233
+ return
234
+ }
235
+
236
+ // UploadByMultipart upload by mutipart blocks
237
+ func (d *Pan115) UploadByMultipart(params *driver115.UploadOSSParams, fileSize int64, stream model.FileStreamer, dirID string, opts ...driver115.UploadMultipartOption) error {
238
+ var (
239
+ chunks []oss.FileChunk
240
+ parts []oss.UploadPart
241
+ imur oss.InitiateMultipartUploadResult
242
+ ossClient *oss.Client
243
+ bucket *oss.Bucket
244
+ ossToken *driver115.UploadOSSTokenResp
245
+ err error
246
+ )
247
+
248
+ tmpF, err := stream.CacheFullInTempFile()
249
+ if err != nil {
250
+ return err
251
+ }
252
+
253
+ options := driver115.DefalutUploadMultipartOptions()
254
+ if len(opts) > 0 {
255
+ for _, f := range opts {
256
+ f(options)
257
+ }
258
+ }
259
+
260
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
261
+ return err
262
+ }
263
+
264
+ if ossClient, err = oss.New(driver115.OSSEndpoint, ossToken.AccessKeyID, ossToken.AccessKeySecret); err != nil {
265
+ return err
266
+ }
267
+
268
+ if bucket, err = ossClient.Bucket(params.Bucket); err != nil {
269
+ return err
270
+ }
271
+
272
+ // ossToken一小时后就会失效,所以每50分钟重新获取一次
273
+ ticker := time.NewTicker(options.TokenRefreshTime)
274
+ defer ticker.Stop()
275
+ // 设置超时
276
+ timeout := time.NewTimer(options.Timeout)
277
+
278
+ if chunks, err = SplitFile(fileSize); err != nil {
279
+ return err
280
+ }
281
+
282
+ if imur, err = bucket.InitiateMultipartUpload(params.Object,
283
+ oss.SetHeader(driver115.OssSecurityTokenHeaderName, ossToken.SecurityToken),
284
+ oss.UserAgentHeader(driver115.OSSUserAgent),
285
+ ); err != nil {
286
+ return err
287
+ }
288
+
289
+ wg := sync.WaitGroup{}
290
+ wg.Add(len(chunks))
291
+
292
+ chunksCh := make(chan oss.FileChunk)
293
+ errCh := make(chan error)
294
+ UploadedPartsCh := make(chan oss.UploadPart)
295
+ quit := make(chan struct{})
296
+
297
+ // producer
298
+ go chunksProducer(chunksCh, chunks)
299
+ go func() {
300
+ wg.Wait()
301
+ quit <- struct{}{}
302
+ }()
303
+
304
+ // consumers
305
+ for i := 0; i < options.ThreadsNum; i++ {
306
+ go func(threadId int) {
307
+ defer func() {
308
+ if r := recover(); r != nil {
309
+ errCh <- fmt.Errorf("recovered in %v", r)
310
+ }
311
+ }()
312
+ for chunk := range chunksCh {
313
+ var part oss.UploadPart // 出现错误就继续尝试,共尝试3次
314
+ for retry := 0; retry < 3; retry++ {
315
+ select {
316
+ case <-ticker.C:
317
+ if ossToken, err = d.client.GetOSSToken(); err != nil { // 到时重新获取ossToken
318
+ errCh <- errors.Wrap(err, "刷新token时出现错误")
319
+ }
320
+ default:
321
+ }
322
+
323
+ buf := make([]byte, chunk.Size)
324
+ if _, err = tmpF.ReadAt(buf, chunk.Offset); err != nil && !errors.Is(err, io.EOF) {
325
+ continue
326
+ }
327
+
328
+ b := bytes.NewBuffer(buf)
329
+ if part, err = bucket.UploadPart(imur, b, chunk.Size, chunk.Number, driver115.OssOption(params, ossToken)...); err == nil {
330
+ break
331
+ }
332
+ }
333
+ if err != nil {
334
+ errCh <- errors.Wrap(err, fmt.Sprintf("上传 %s 的第%d个分片时出现错误:%v", stream.GetName(), chunk.Number, err))
335
+ }
336
+ UploadedPartsCh <- part
337
+ }
338
+ }(i)
339
+ }
340
+
341
+ go func() {
342
+ for part := range UploadedPartsCh {
343
+ parts = append(parts, part)
344
+ wg.Done()
345
+ }
346
+ }()
347
+ LOOP:
348
+ for {
349
+ select {
350
+ case <-ticker.C:
351
+ // 到时重新获取ossToken
352
+ if ossToken, err = d.client.GetOSSToken(); err != nil {
353
+ return err
354
+ }
355
+ case <-quit:
356
+ break LOOP
357
+ case <-errCh:
358
+ return err
359
+ case <-timeout.C:
360
+ return fmt.Errorf("time out")
361
+ }
362
+ }
363
+
364
+ // EOF错误是xml的Unmarshal导致的,响应其实是json格式,所以实际上上传是成功的
365
+ if _, err = bucket.CompleteMultipartUpload(imur, parts, driver115.OssOption(params, ossToken)...); err != nil && !errors.Is(err, io.EOF) {
366
+ // 当文件名含有 &< 这两个字符之一时响应的xml解析会出现错误,实际上上传是成功的
367
+ if filename := filepath.Base(stream.GetName()); !strings.ContainsAny(filename, "&<") {
368
+ return err
369
+ }
370
+ }
371
+ return d.checkUploadStatus(dirID, params.SHA1)
372
+ }
373
+ func chunksProducer(ch chan oss.FileChunk, chunks []oss.FileChunk) {
374
+ for _, chunk := range chunks {
375
+ ch <- chunk
376
+ }
377
+ }
378
+ func (d *Pan115) checkUploadStatus(dirID, sha1 string) error {
379
+ // 验证上传是否成功
380
+ req := d.client.NewRequest().ForceContentType("application/json;charset=UTF-8")
381
+ opts := []driver115.GetFileOptions{
382
+ driver115.WithOrder(driver115.FileOrderByTime),
383
+ driver115.WithShowDirEnable(false),
384
+ driver115.WithAsc(false),
385
+ driver115.WithLimit(500),
386
+ }
387
+ fResp, err := driver115.GetFiles(req, dirID, opts...)
388
+ if err != nil {
389
+ return err
390
+ }
391
+ for _, fileInfo := range fResp.Files {
392
+ if fileInfo.Sha1 == sha1 {
393
+ return nil
394
+ }
395
+ }
396
+ return driver115.ErrUploadFailed
397
+ }
398
+
399
+ func SplitFile(fileSize int64) (chunks []oss.FileChunk, err error) {
400
+ for i := int64(1); i < 10; i++ {
401
+ if fileSize < i*utils.GB { // 文件大小小于iGB时分为i*1000片
402
+ if chunks, err = SplitFileByPartNum(fileSize, int(i*1000)); err != nil {
403
+ return
404
+ }
405
+ break
406
+ }
407
+ }
408
+ if fileSize > 9*utils.GB { // 文件大小大于9GB时分为10000片
409
+ if chunks, err = SplitFileByPartNum(fileSize, 10000); err != nil {
410
+ return
411
+ }
412
+ }
413
+ // 单个分片大小不能小于100KB
414
+ if chunks[0].Size < 100*utils.KB {
415
+ if chunks, err = SplitFileByPartSize(fileSize, 100*utils.KB); err != nil {
416
+ return
417
+ }
418
+ }
419
+ return
420
+ }
421
+
422
+ // SplitFileByPartNum splits big file into parts by the num of parts.
423
+ // Split the file with specified parts count, returns the split result when error is nil.
424
+ func SplitFileByPartNum(fileSize int64, chunkNum int) ([]oss.FileChunk, error) {
425
+ if chunkNum <= 0 || chunkNum > 10000 {
426
+ return nil, errors.New("chunkNum invalid")
427
+ }
428
+
429
+ if int64(chunkNum) > fileSize {
430
+ return nil, errors.New("oss: chunkNum invalid")
431
+ }
432
+
433
+ var chunks []oss.FileChunk
434
+ var chunk = oss.FileChunk{}
435
+ var chunkN = (int64)(chunkNum)
436
+ for i := int64(0); i < chunkN; i++ {
437
+ chunk.Number = int(i + 1)
438
+ chunk.Offset = i * (fileSize / chunkN)
439
+ if i == chunkN-1 {
440
+ chunk.Size = fileSize/chunkN + fileSize%chunkN
441
+ } else {
442
+ chunk.Size = fileSize / chunkN
443
+ }
444
+ chunks = append(chunks, chunk)
445
+ }
446
+
447
+ return chunks, nil
448
+ }
449
+
450
+ // SplitFileByPartSize splits big file into parts by the size of parts.
451
+ // Splits the file by the part size. Returns the FileChunk when error is nil.
452
+ func SplitFileByPartSize(fileSize int64, chunkSize int64) ([]oss.FileChunk, error) {
453
+ if chunkSize <= 0 {
454
+ return nil, errors.New("chunkSize invalid")
455
+ }
456
+
457
+ var chunkN = fileSize / chunkSize
458
+ if chunkN >= 10000 {
459
+ return nil, errors.New("Too many parts, please increase part size")
460
+ }
461
+
462
+ var chunks []oss.FileChunk
463
+ var chunk = oss.FileChunk{}
464
+ for i := int64(0); i < chunkN; i++ {
465
+ chunk.Number = int(i + 1)
466
+ chunk.Offset = i * chunkSize
467
+ chunk.Size = chunkSize
468
+ chunks = append(chunks, chunk)
469
+ }
470
+
471
+ if fileSize%chunkSize > 0 {
472
+ chunk.Number = len(chunks) + 1
473
+ chunk.Offset = int64(len(chunks)) * chunkSize
474
+ chunk.Size = fileSize % chunkSize
475
+ chunks = append(chunks, chunk)
476
+ }
477
+
478
+ return chunks, nil
479
+ }
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,linux,mac,windows,tv" default:"linux" help:"select the QR code device, default linux"`
12
+ PageSize int64 `json:"page_size" type:"number" default:"20" help:"list api per page size of 115 driver"`
13
+ LimitRate float64 `json:"limit_rate" type:"number" 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", cr.UID, cr.CID, cr.SEID)
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,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(dir.GetID())
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
+ req.SetBody(data).SetHeaders(headers)
86
+ }, nil)
87
+ if err != nil {
88
+ return nil, err
89
+ }
90
+ downloadUrl := utils.Json.Get(resp, "data", "DownloadUrl").ToString()
91
+ u, err := url.Parse(downloadUrl)
92
+ if err != nil {
93
+ return nil, err
94
+ }
95
+ nu := u.Query().Get("params")
96
+ if nu != "" {
97
+ du, _ := base64.StdEncoding.DecodeString(nu)
98
+ u, err = url.Parse(string(du))
99
+ if err != nil {
100
+ return nil, err
101
+ }
102
+ }
103
+ u_ := u.String()
104
+ log.Debug("download url: ", u_)
105
+ res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://www.123pan.com/").Get(u_)
106
+ if err != nil {
107
+ return nil, err
108
+ }
109
+ log.Debug(res.String())
110
+ link := model.Link{
111
+ URL: u_,
112
+ }
113
+ log.Debugln("res code: ", res.StatusCode())
114
+ if res.StatusCode() == 302 {
115
+ link.URL = res.Header().Get("location")
116
+ } else if res.StatusCode() < 300 {
117
+ link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
118
+ }
119
+ link.Header = http.Header{
120
+ "Referer": []string{"https://www.123pan.com/"},
121
+ }
122
+ return &link, nil
123
+ } else {
124
+ return nil, fmt.Errorf("can't convert obj")
125
+ }
126
+ }
127
+
128
+ func (d *Pan123) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
129
+ data := base.Json{
130
+ "driveId": 0,
131
+ "etag": "",
132
+ "fileName": dirName,
133
+ "parentFileId": parentDir.GetID(),
134
+ "size": 0,
135
+ "type": 1,
136
+ }
137
+ _, err := d.request(Mkdir, http.MethodPost, func(req *resty.Request) {
138
+ req.SetBody(data)
139
+ }, nil)
140
+ return err
141
+ }
142
+
143
+ func (d *Pan123) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
144
+ data := base.Json{
145
+ "fileIdList": []base.Json{{"FileId": srcObj.GetID()}},
146
+ "parentFileId": dstDir.GetID(),
147
+ }
148
+ _, err := d.request(Move, http.MethodPost, func(req *resty.Request) {
149
+ req.SetBody(data)
150
+ }, nil)
151
+ return err
152
+ }
153
+
154
+ func (d *Pan123) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
155
+ data := base.Json{
156
+ "driveId": 0,
157
+ "fileId": srcObj.GetID(),
158
+ "fileName": newName,
159
+ }
160
+ _, err := d.request(Rename, http.MethodPost, func(req *resty.Request) {
161
+ req.SetBody(data)
162
+ }, nil)
163
+ return err
164
+ }
165
+
166
+ func (d *Pan123) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
167
+ return errs.NotSupport
168
+ }
169
+
170
+ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
171
+ if f, ok := obj.(File); ok {
172
+ data := base.Json{
173
+ "driveId": 0,
174
+ "operation": true,
175
+ "fileTrashInfoList": []File{f},
176
+ }
177
+ _, err := d.request(Trash, http.MethodPost, func(req *resty.Request) {
178
+ req.SetBody(data)
179
+ }, nil)
180
+ return err
181
+ } else {
182
+ return fmt.Errorf("can't convert obj")
183
+ }
184
+ }
185
+
186
+ func (d *Pan123) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
187
+ // const DEFAULT int64 = 10485760
188
+ h := md5.New()
189
+ // need to calculate md5 of the full content
190
+ tempFile, err := stream.CacheFullInTempFile()
191
+ if err != nil {
192
+ return err
193
+ }
194
+ defer func() {
195
+ _ = tempFile.Close()
196
+ }()
197
+ if _, err = utils.CopyWithBuffer(h, tempFile); err != nil {
198
+ return err
199
+ }
200
+ _, err = tempFile.Seek(0, io.SeekStart)
201
+ if err != nil {
202
+ return err
203
+ }
204
+ etag := hex.EncodeToString(h.Sum(nil))
205
+ data := base.Json{
206
+ "driveId": 0,
207
+ "duplicate": 2, // 2->覆盖 1->重命名 0->默认
208
+ "etag": etag,
209
+ "fileName": stream.GetName(),
210
+ "parentFileId": dstDir.GetID(),
211
+ "size": stream.GetSize(),
212
+ "type": 0,
213
+ }
214
+ var resp UploadResp
215
+ res, err := d.request(UploadRequest, http.MethodPost, func(req *resty.Request) {
216
+ req.SetBody(data).SetContext(ctx)
217
+ }, &resp)
218
+ if err != nil {
219
+ return err
220
+ }
221
+ log.Debugln("upload request res: ", string(res))
222
+ if resp.Data.Reuse || resp.Data.Key == "" {
223
+ return nil
224
+ }
225
+ if resp.Data.AccessKeyId == "" || resp.Data.SecretAccessKey == "" || resp.Data.SessionToken == "" {
226
+ err = d.newUpload(ctx, &resp, stream, tempFile, up)
227
+ return err
228
+ } else {
229
+ cfg := &aws.Config{
230
+ Credentials: credentials.NewStaticCredentials(resp.Data.AccessKeyId, resp.Data.SecretAccessKey, resp.Data.SessionToken),
231
+ Region: aws.String("123pan"),
232
+ Endpoint: aws.String(resp.Data.EndPoint),
233
+ S3ForcePathStyle: aws.Bool(true),
234
+ }
235
+ s, err := session.NewSession(cfg)
236
+ if err != nil {
237
+ return err
238
+ }
239
+ uploader := s3manager.NewUploader(s)
240
+ if stream.GetSize() > s3manager.MaxUploadParts*s3manager.DefaultUploadPartSize {
241
+ uploader.PartSize = stream.GetSize() / (s3manager.MaxUploadParts - 1)
242
+ }
243
+ input := &s3manager.UploadInput{
244
+ Bucket: &resp.Data.Bucket,
245
+ Key: &resp.Data.Key,
246
+ Body: tempFile,
247
+ }
248
+ _, err = uploader.UploadWithContext(ctx, input)
249
+ }
250
+ if err != nil {
251
+ return err
252
+ }
253
+ _, err = d.request(UploadComplete, http.MethodPost, func(req *resty.Request) {
254
+ req.SetBody(base.Json{
255
+ "fileId": resp.Data.FileId,
256
+ }).SetContext(ctx)
257
+ }, nil)
258
+ return err
259
+ }
260
+
261
+ func (d *Pan123) APIRateLimit(api string) bool {
262
+ limiter, _ := d.apiRateLimit.LoadOrStore(api,
263
+ rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
264
+ ins := limiter.(*rate.Limiter)
265
+ return ins.Allow()
266
+ }
267
+
268
+ var _ driver.Driver = (*Pan123)(nil)
drivers/123/meta.go ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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_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
+ }
21
+
22
+ func init() {
23
+ op.RegisterDriver(func() driver.Driver {
24
+ return &Pan123{}
25
+ })
26
+ }
drivers/123/types.go ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ InfoList []File `json:"InfoList"`
91
+ Next string `json:"Next"`
92
+ } `json:"data"`
93
+ }
94
+
95
+ //type DownResp struct {
96
+ // //BaseResp
97
+ // Data struct {
98
+ // DownloadUrl string `json:"DownloadUrl"`
99
+ // } `json:"data"`
100
+ //}
101
+
102
+ type UploadResp struct {
103
+ //BaseResp
104
+ Data struct {
105
+ AccessKeyId string `json:"AccessKeyId"`
106
+ Bucket string `json:"Bucket"`
107
+ Key string `json:"Key"`
108
+ SecretAccessKey string `json:"SecretAccessKey"`
109
+ SessionToken string `json:"SessionToken"`
110
+ FileId int64 `json:"FileId"`
111
+ Reuse bool `json:"Reuse"`
112
+ EndPoint string `json:"EndPoint"`
113
+ StorageNode string `json:"StorageNode"`
114
+ UploadId string `json:"UploadId"`
115
+ } `json:"data"`
116
+ }
117
+
118
+ type S3PreSignedURLs struct {
119
+ Data struct {
120
+ PreSignedUrls map[string]string `json:"presignedUrls"`
121
+ } `json:"data"`
122
+ }
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,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _123
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "hash/crc32"
7
+ "math"
8
+ "math/rand"
9
+ "net/http"
10
+ "net/url"
11
+ "strconv"
12
+ "strings"
13
+ "time"
14
+
15
+ "github.com/alist-org/alist/v3/drivers/base"
16
+ "github.com/alist-org/alist/v3/pkg/utils"
17
+ resty "github.com/go-resty/resty/v2"
18
+ jsoniter "github.com/json-iterator/go"
19
+ )
20
+
21
+ // do others that not defined in Driver interface
22
+
23
+ const (
24
+ Api = "https://www.123pan.com/api"
25
+ AApi = "https://www.123pan.com/a/api"
26
+ BApi = "https://www.123pan.com/b/api"
27
+ MainApi = BApi
28
+ SignIn = MainApi + "/user/sign_in"
29
+ Logout = MainApi + "/user/logout"
30
+ UserInfo = MainApi + "/user/info"
31
+ FileList = MainApi + "/file/list/new"
32
+ DownloadInfo = MainApi + "/file/download_info"
33
+ Mkdir = MainApi + "/file/upload_request"
34
+ Move = MainApi + "/file/mod_pid"
35
+ Rename = MainApi + "/file/rename"
36
+ Trash = MainApi + "/file/trash"
37
+ UploadRequest = MainApi + "/file/upload_request"
38
+ UploadComplete = MainApi + "/file/upload_complete"
39
+ S3PreSignedUrls = MainApi + "/file/s3_repare_upload_parts_batch"
40
+ S3Auth = MainApi + "/file/s3_upload_object/auth"
41
+ UploadCompleteV2 = MainApi + "/file/upload_complete/v2"
42
+ S3Complete = MainApi + "/file/s3_complete_multipart_upload"
43
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
44
+ )
45
+
46
+ func signPath(path string, os string, version string) (k string, v string) {
47
+ 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'}
48
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
49
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
50
+ timestamp := fmt.Sprint(now.Unix())
51
+ nowStr := []byte(now.Format("200601021504"))
52
+ for i := 0; i < len(nowStr); i++ {
53
+ nowStr[i] = table[nowStr[i]-48]
54
+ }
55
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
56
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
57
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
58
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
59
+ }
60
+
61
+ func GetApi(rawUrl string) string {
62
+ u, _ := url.Parse(rawUrl)
63
+ query := u.Query()
64
+ query.Add(signPath(u.Path, "web", "3"))
65
+ u.RawQuery = query.Encode()
66
+ return u.String()
67
+ }
68
+
69
+ //func GetApi(url string) string {
70
+ // vm := js.New()
71
+ // vm.Set("url", url[22:])
72
+ // r, err := vm.RunString(`
73
+ // (function(e){
74
+ // function A(t, e) {
75
+ // e = 1 < arguments.length && void 0 !== e ? e : 10;
76
+ // for (var n = function() {
77
+ // for (var t = [], e = 0; e < 256; e++) {
78
+ // for (var n = e, r = 0; r < 8; r++)
79
+ // n = 1 & n ? 3988292384 ^ n >>> 1 : n >>> 1;
80
+ // t[e] = n
81
+ // }
82
+ // return t
83
+ // }(), r = function(t) {
84
+ // t = t.replace(/\\r\\n/g, "\\n");
85
+ // for (var e = "", n = 0; n < t.length; n++) {
86
+ // var r = t.charCodeAt(n);
87
+ // 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)
88
+ // }
89
+ // return e
90
+ // }(t), a = -1, i = 0; i < r.length; i++)
91
+ // a = a >>> 8 ^ n[255 & (a ^ r.charCodeAt(i))];
92
+ // return (a = (-1 ^ a) >>> 0).toString(e)
93
+ // }
94
+ //
95
+ // function v(t) {
96
+ // return (v = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(t) {
97
+ // return typeof t
98
+ // }
99
+ // : function(t) {
100
+ // return t && "function" == typeof Symbol && t.constructor === Symbol && t !== Symbol.prototype ? "symbol" : typeof t
101
+ // }
102
+ // )(t)
103
+ // }
104
+ //
105
+ // for (p in a = Math.round(1e7 * Math.random()),
106
+ // o = Math.round(((new Date).getTime() + 60 * (new Date).getTimezoneOffset() * 1e3 + 288e5) / 1e3).toString(),
107
+ // 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"],
108
+ // u = function(t, e, n) {
109
+ // var r;
110
+ // n = 2 < arguments.length && void 0 !== n ? n : 8;
111
+ // return 0 === arguments.length ? null : (r = "object" === v(t) ? t : (10 === "".concat(t).length && (t = 1e3 * Number.parseInt(t)),
112
+ // new Date(t)),
113
+ // t += 6e4 * new Date(t).getTimezoneOffset(),
114
+ // {
115
+ // y: (r = new Date(t + 36e5 * n)).getFullYear(),
116
+ // m: r.getMonth() + 1 < 10 ? "0".concat(r.getMonth() + 1) : r.getMonth() + 1,
117
+ // d: r.getDate() < 10 ? "0".concat(r.getDate()) : r.getDate(),
118
+ // h: r.getHours() < 10 ? "0".concat(r.getHours()) : r.getHours(),
119
+ // f: r.getMinutes() < 10 ? "0".concat(r.getMinutes()) : r.getMinutes()
120
+ // })
121
+ // }(o),
122
+ // h = u.y,
123
+ // g = u.m,
124
+ // l = u.d,
125
+ // c = u.h,
126
+ // u = u.f,
127
+ // d = [h, g, l, c, u].join(""),
128
+ // f = [],
129
+ // d)
130
+ // f.push(m[Number(d[p])]);
131
+ // return h = A(f.join("")),
132
+ // g = A("".concat(o, "|").concat(a, "|").concat(e, "|").concat("web", "|").concat("3", "|").concat(h)),
133
+ // "".concat(h, "=").concat(o, "-").concat(a, "-").concat(g);
134
+ // })(url)
135
+ // `)
136
+ // if err != nil {
137
+ // fmt.Println(err)
138
+ // return url
139
+ // }
140
+ // v, _ := r.Export().(string)
141
+ // return url + "?" + v
142
+ //}
143
+
144
+ func (d *Pan123) login() error {
145
+ var body base.Json
146
+ if utils.IsEmailFormat(d.Username) {
147
+ body = base.Json{
148
+ "mail": d.Username,
149
+ "password": d.Password,
150
+ "type": 2,
151
+ }
152
+ } else {
153
+ body = base.Json{
154
+ "passport": d.Username,
155
+ "password": d.Password,
156
+ "remember": true,
157
+ }
158
+ }
159
+ res, err := base.RestyClient.R().
160
+ SetHeaders(map[string]string{
161
+ "origin": "https://www.123pan.com",
162
+ "referer": "https://www.123pan.com/",
163
+ "user-agent": "Dart/2.19(dart:io)-alist",
164
+ "platform": "web",
165
+ "app-version": "3",
166
+ //"user-agent": base.UserAgent,
167
+ }).
168
+ SetBody(body).Post(SignIn)
169
+ if err != nil {
170
+ return err
171
+ }
172
+ if utils.Json.Get(res.Body(), "code").ToInt() != 200 {
173
+ err = fmt.Errorf(utils.Json.Get(res.Body(), "message").ToString())
174
+ } else {
175
+ d.AccessToken = utils.Json.Get(res.Body(), "data", "token").ToString()
176
+ }
177
+ return err
178
+ }
179
+
180
+ //func authKey(reqUrl string) (*string, error) {
181
+ // reqURL, err := url.Parse(reqUrl)
182
+ // if err != nil {
183
+ // return nil, err
184
+ // }
185
+ //
186
+ // nowUnix := time.Now().Unix()
187
+ // random := rand.Intn(0x989680)
188
+ //
189
+ // p4 := fmt.Sprintf("%d|%d|%s|%s|%s|%s", nowUnix, random, reqURL.Path, "web", "3", AuthKeySalt)
190
+ // authKey := fmt.Sprintf("%d-%d-%x", nowUnix, random, md5.Sum([]byte(p4)))
191
+ // return &authKey, nil
192
+ //}
193
+
194
+ func (d *Pan123) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
195
+ req := base.RestyClient.R()
196
+ req.SetHeaders(map[string]string{
197
+ "origin": "https://www.123pan.com",
198
+ "referer": "https://www.123pan.com/",
199
+ "authorization": "Bearer " + d.AccessToken,
200
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
201
+ "platform": "web",
202
+ "app-version": "3",
203
+ //"user-agent": base.UserAgent,
204
+ })
205
+ if callback != nil {
206
+ callback(req)
207
+ }
208
+ if resp != nil {
209
+ req.SetResult(resp)
210
+ }
211
+ //authKey, err := authKey(url)
212
+ //if err != nil {
213
+ // return nil, err
214
+ //}
215
+ //req.SetQueryParam("auth-key", *authKey)
216
+ res, err := req.Execute(method, GetApi(url))
217
+ if err != nil {
218
+ return nil, err
219
+ }
220
+ body := res.Body()
221
+ code := utils.Json.Get(body, "code").ToInt()
222
+ if code != 0 {
223
+ if code == 401 {
224
+ err := d.login()
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ return d.request(url, method, callback, resp)
229
+ }
230
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
231
+ }
232
+ return body, nil
233
+ }
234
+
235
+ func (d *Pan123) getFiles(parentId string) ([]File, error) {
236
+ page := 1
237
+ res := make([]File, 0)
238
+ // 2024-02-06 fix concurrency by 123pan
239
+ for {
240
+ if !d.APIRateLimit(FileList) {
241
+ time.Sleep(time.Millisecond * 200)
242
+ continue
243
+ }
244
+ var resp Files
245
+ query := map[string]string{
246
+ "driveId": "0",
247
+ "limit": "100",
248
+ "next": "0",
249
+ "orderBy": d.OrderBy,
250
+ "orderDirection": d.OrderDirection,
251
+ "parentFileId": parentId,
252
+ "trashed": "false",
253
+ "SearchData": "",
254
+ "Page": strconv.Itoa(page),
255
+ "OnlyLookAbnormalFile": "0",
256
+ "event": "homeListFile",
257
+ "operateType": "4",
258
+ "inDirectSpace": "false",
259
+ }
260
+ _, err := d.request(FileList, http.MethodGet, func(req *resty.Request) {
261
+ req.SetQueryParams(query)
262
+ }, &resp)
263
+ if err != nil {
264
+ return nil, err
265
+ }
266
+ page++
267
+ res = append(res, resp.Data.InfoList...)
268
+ if len(resp.Data.InfoList) == 0 || resp.Data.Next == "-1" {
269
+ break
270
+ }
271
+ }
272
+ return res, nil
273
+ }
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,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(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(api string) bool {
154
+ limiter, _ := d.apiRateLimit.LoadOrStore(api,
155
+ rate.NewLimiter(rate.Every(time.Millisecond*700), 1))
156
+ ins := limiter.(*rate.Limiter)
157
+ return ins.Allow()
158
+ }
159
+
160
+ 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
+ "errors"
5
+ "fmt"
6
+ "hash/crc32"
7
+ "math"
8
+ "math/rand"
9
+ "net/http"
10
+ "net/url"
11
+ "strconv"
12
+ "strings"
13
+ "time"
14
+
15
+ "github.com/alist-org/alist/v3/drivers/base"
16
+ "github.com/alist-org/alist/v3/pkg/utils"
17
+ "github.com/go-resty/resty/v2"
18
+ jsoniter "github.com/json-iterator/go"
19
+ )
20
+
21
+ const (
22
+ Api = "https://www.123pan.com/api"
23
+ AApi = "https://www.123pan.com/a/api"
24
+ BApi = "https://www.123pan.com/b/api"
25
+ MainApi = BApi
26
+ FileList = MainApi + "/share/get"
27
+ DownloadInfo = MainApi + "/share/download/info"
28
+ //AuthKeySalt = "8-8D$sL8gPjom7bk#cY"
29
+ )
30
+
31
+ func signPath(path string, os string, version string) (k string, v string) {
32
+ 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'}
33
+ random := fmt.Sprintf("%.f", math.Round(1e7*rand.Float64()))
34
+ now := time.Now().In(time.FixedZone("CST", 8*3600))
35
+ timestamp := fmt.Sprint(now.Unix())
36
+ nowStr := []byte(now.Format("200601021504"))
37
+ for i := 0; i < len(nowStr); i++ {
38
+ nowStr[i] = table[nowStr[i]-48]
39
+ }
40
+ timeSign := fmt.Sprint(crc32.ChecksumIEEE(nowStr))
41
+ data := strings.Join([]string{timestamp, random, path, os, version, timeSign}, "|")
42
+ dataSign := fmt.Sprint(crc32.ChecksumIEEE([]byte(data)))
43
+ return timeSign, strings.Join([]string{timestamp, random, dataSign}, "-")
44
+ }
45
+
46
+ func GetApi(rawUrl string) string {
47
+ u, _ := url.Parse(rawUrl)
48
+ query := u.Query()
49
+ query.Add(signPath(u.Path, "web", "3"))
50
+ u.RawQuery = query.Encode()
51
+ return u.String()
52
+ }
53
+
54
+ func (d *Pan123Share) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
55
+ req := base.RestyClient.R()
56
+ req.SetHeaders(map[string]string{
57
+ "origin": "https://www.123pan.com",
58
+ "referer": "https://www.123pan.com/",
59
+ "authorization": "Bearer " + d.AccessToken,
60
+ "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) alist-client",
61
+ "platform": "web",
62
+ "app-version": "3",
63
+ //"user-agent": base.UserAgent,
64
+ })
65
+ if callback != nil {
66
+ callback(req)
67
+ }
68
+ if resp != nil {
69
+ req.SetResult(resp)
70
+ }
71
+ res, err := req.Execute(method, GetApi(url))
72
+ if err != nil {
73
+ return nil, err
74
+ }
75
+ body := res.Body()
76
+ code := utils.Json.Get(body, "code").ToInt()
77
+ if code != 0 {
78
+ return nil, errors.New(jsoniter.Get(body, "message").ToString())
79
+ }
80
+ return body, nil
81
+ }
82
+
83
+ func (d *Pan123Share) getFiles(parentId string) ([]File, error) {
84
+ page := 1
85
+ res := make([]File, 0)
86
+ for {
87
+ if !d.APIRateLimit(FileList) {
88
+ time.Sleep(time.Millisecond * 200)
89
+ continue
90
+ }
91
+ var resp Files
92
+ query := map[string]string{
93
+ "limit": "100",
94
+ "next": "0",
95
+ "orderBy": d.OrderBy,
96
+ "orderDirection": d.OrderDirection,
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,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/utils"
18
+ "github.com/alist-org/alist/v3/pkg/cron"
19
+ log "github.com/sirupsen/logrus"
20
+ )
21
+
22
+ type Yun139 struct {
23
+ model.Storage
24
+ Addition
25
+ cron *cron.Cron
26
+ Account string
27
+ }
28
+
29
+ func (d *Yun139) Config() driver.Config {
30
+ return config
31
+ }
32
+
33
+ func (d *Yun139) GetAddition() driver.Additional {
34
+ return &d.Addition
35
+ }
36
+
37
+ func (d *Yun139) Init(ctx context.Context) error {
38
+ if d.Authorization == "" {
39
+ return fmt.Errorf("authorization is empty")
40
+ }
41
+ d.cron = cron.NewCron(time.Hour * 24 * 7)
42
+ d.cron.Do(func() {
43
+ err := d.refreshToken()
44
+ if err != nil {
45
+ log.Errorf("%+v", err)
46
+ }
47
+ })
48
+ switch d.Addition.Type {
49
+ case MetaPersonalNew:
50
+ if len(d.Addition.RootFolderID) == 0 {
51
+ d.RootFolderID = "/"
52
+ }
53
+ return nil
54
+ case MetaPersonal:
55
+ if len(d.Addition.RootFolderID) == 0 {
56
+ d.RootFolderID = "root"
57
+ }
58
+ fallthrough
59
+ case MetaFamily:
60
+ decode, err := base64.StdEncoding.DecodeString(d.Authorization)
61
+ if err != nil {
62
+ return err
63
+ }
64
+ decodeStr := string(decode)
65
+ splits := strings.Split(decodeStr, ":")
66
+ if len(splits) < 2 {
67
+ return fmt.Errorf("authorization is invalid, splits < 2")
68
+ }
69
+ d.Account = splits[1]
70
+ _, err = d.post("/orchestration/personalCloud/user/v1.0/qryUserExternInfo", base.Json{
71
+ "qryUserExternInfoReq": base.Json{
72
+ "commonAccountInfo": base.Json{
73
+ "account": d.Account,
74
+ "accountType": 1,
75
+ },
76
+ },
77
+ }, nil)
78
+ return err
79
+ default:
80
+ return errs.NotImplement
81
+ }
82
+ }
83
+
84
+ func (d *Yun139) Drop(ctx context.Context) error {
85
+ if d.cron != nil {
86
+ d.cron.Stop()
87
+ }
88
+ return nil
89
+ }
90
+
91
+ func (d *Yun139) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
92
+ switch d.Addition.Type {
93
+ case MetaPersonalNew:
94
+ return d.personalGetFiles(dir.GetID())
95
+ case MetaPersonal:
96
+ return d.getFiles(dir.GetID())
97
+ case MetaFamily:
98
+ return d.familyGetFiles(dir.GetID())
99
+ default:
100
+ return nil, errs.NotImplement
101
+ }
102
+ }
103
+
104
+ func (d *Yun139) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
105
+ var url string
106
+ var err error
107
+ switch d.Addition.Type {
108
+ case MetaPersonalNew:
109
+ url, err = d.personalGetLink(file.GetID())
110
+ case MetaPersonal:
111
+ fallthrough
112
+ case MetaFamily:
113
+ url, err = d.getLink(file.GetID())
114
+ default:
115
+ return nil, errs.NotImplement
116
+ }
117
+ if err != nil {
118
+ return nil, err
119
+ }
120
+ return &model.Link{URL: url}, nil
121
+ }
122
+
123
+ func (d *Yun139) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
124
+ var err error
125
+ switch d.Addition.Type {
126
+ case MetaPersonalNew:
127
+ data := base.Json{
128
+ "parentFileId": parentDir.GetID(),
129
+ "name": dirName,
130
+ "description": "",
131
+ "type": "folder",
132
+ "fileRenameMode": "force_rename",
133
+ }
134
+ pathname := "/hcy/file/create"
135
+ _, err = d.personalPost(pathname, data, nil)
136
+ case MetaPersonal:
137
+ data := base.Json{
138
+ "createCatalogExtReq": base.Json{
139
+ "parentCatalogID": parentDir.GetID(),
140
+ "newCatalogName": dirName,
141
+ "commonAccountInfo": base.Json{
142
+ "account": d.Account,
143
+ "accountType": 1,
144
+ },
145
+ },
146
+ }
147
+ pathname := "/orchestration/personalCloud/catalog/v1.0/createCatalogExt"
148
+ _, err = d.post(pathname, data, nil)
149
+ case MetaFamily:
150
+ data := base.Json{
151
+ "cloudID": d.CloudID,
152
+ "commonAccountInfo": base.Json{
153
+ "account": d.Account,
154
+ "accountType": 1,
155
+ },
156
+ "docLibName": dirName,
157
+ }
158
+ pathname := "/orchestration/familyCloud/cloudCatalog/v1.0/createCloudDoc"
159
+ _, err = d.post(pathname, data, nil)
160
+ default:
161
+ err = errs.NotImplement
162
+ }
163
+ return err
164
+ }
165
+
166
+ func (d *Yun139) Move(ctx context.Context, srcObj, dstDir model.Obj) (model.Obj, error) {
167
+ switch d.Addition.Type {
168
+ case MetaPersonalNew:
169
+ data := base.Json{
170
+ "fileIds": []string{srcObj.GetID()},
171
+ "toParentFileId": dstDir.GetID(),
172
+ }
173
+ pathname := "/hcy/file/batchMove"
174
+ _, err := d.personalPost(pathname, data, nil)
175
+ if err != nil {
176
+ return nil, err
177
+ }
178
+ return srcObj, nil
179
+ case MetaPersonal:
180
+ var contentInfoList []string
181
+ var catalogInfoList []string
182
+ if srcObj.IsDir() {
183
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
184
+ } else {
185
+ contentInfoList = append(contentInfoList, srcObj.GetID())
186
+ }
187
+ data := base.Json{
188
+ "createBatchOprTaskReq": base.Json{
189
+ "taskType": 3,
190
+ "actionType": "304",
191
+ "taskInfo": base.Json{
192
+ "contentInfoList": contentInfoList,
193
+ "catalogInfoList": catalogInfoList,
194
+ "newCatalogID": dstDir.GetID(),
195
+ },
196
+ "commonAccountInfo": base.Json{
197
+ "account": d.Account,
198
+ "accountType": 1,
199
+ },
200
+ },
201
+ }
202
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
203
+ _, err := d.post(pathname, data, nil)
204
+ if err != nil {
205
+ return nil, err
206
+ }
207
+ return srcObj, nil
208
+ default:
209
+ return nil, errs.NotImplement
210
+ }
211
+ }
212
+
213
+ func (d *Yun139) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
214
+ var err error
215
+ switch d.Addition.Type {
216
+ case MetaPersonalNew:
217
+ data := base.Json{
218
+ "fileId": srcObj.GetID(),
219
+ "name": newName,
220
+ "description": "",
221
+ }
222
+ pathname := "/hcy/file/update"
223
+ _, err = d.personalPost(pathname, data, nil)
224
+ case MetaPersonal:
225
+ var data base.Json
226
+ var pathname string
227
+ if srcObj.IsDir() {
228
+ data = base.Json{
229
+ "catalogID": srcObj.GetID(),
230
+ "catalogName": newName,
231
+ "commonAccountInfo": base.Json{
232
+ "account": d.Account,
233
+ "accountType": 1,
234
+ },
235
+ }
236
+ pathname = "/orchestration/personalCloud/catalog/v1.0/updateCatalogInfo"
237
+ } else {
238
+ data = base.Json{
239
+ "contentID": srcObj.GetID(),
240
+ "contentName": newName,
241
+ "commonAccountInfo": base.Json{
242
+ "account": d.Account,
243
+ "accountType": 1,
244
+ },
245
+ }
246
+ pathname = "/orchestration/personalCloud/content/v1.0/updateContentInfo"
247
+ }
248
+ _, err = d.post(pathname, data, nil)
249
+ default:
250
+ err = errs.NotImplement
251
+ }
252
+ return err
253
+ }
254
+
255
+ func (d *Yun139) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
256
+ var err error
257
+ switch d.Addition.Type {
258
+ case MetaPersonalNew:
259
+ data := base.Json{
260
+ "fileIds": []string{srcObj.GetID()},
261
+ "toParentFileId": dstDir.GetID(),
262
+ }
263
+ pathname := "/hcy/file/batchCopy"
264
+ _, err := d.personalPost(pathname, data, nil)
265
+ return err
266
+ case MetaPersonal:
267
+ var contentInfoList []string
268
+ var catalogInfoList []string
269
+ if srcObj.IsDir() {
270
+ catalogInfoList = append(catalogInfoList, srcObj.GetID())
271
+ } else {
272
+ contentInfoList = append(contentInfoList, srcObj.GetID())
273
+ }
274
+ data := base.Json{
275
+ "createBatchOprTaskReq": base.Json{
276
+ "taskType": 3,
277
+ "actionType": 309,
278
+ "taskInfo": base.Json{
279
+ "contentInfoList": contentInfoList,
280
+ "catalogInfoList": catalogInfoList,
281
+ "newCatalogID": dstDir.GetID(),
282
+ },
283
+ "commonAccountInfo": base.Json{
284
+ "account": d.Account,
285
+ "accountType": 1,
286
+ },
287
+ },
288
+ }
289
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
290
+ _, err = d.post(pathname, data, nil)
291
+ default:
292
+ err = errs.NotImplement
293
+ }
294
+ return err
295
+ }
296
+
297
+ func (d *Yun139) Remove(ctx context.Context, obj model.Obj) error {
298
+ switch d.Addition.Type {
299
+ case MetaPersonalNew:
300
+ data := base.Json{
301
+ "fileIds": []string{obj.GetID()},
302
+ }
303
+ pathname := "/hcy/recyclebin/batchTrash"
304
+ _, err := d.personalPost(pathname, data, nil)
305
+ return err
306
+ case MetaPersonal:
307
+ fallthrough
308
+ case MetaFamily:
309
+ var contentInfoList []string
310
+ var catalogInfoList []string
311
+ if obj.IsDir() {
312
+ catalogInfoList = append(catalogInfoList, obj.GetID())
313
+ } else {
314
+ contentInfoList = append(contentInfoList, obj.GetID())
315
+ }
316
+ data := base.Json{
317
+ "createBatchOprTaskReq": base.Json{
318
+ "taskType": 2,
319
+ "actionType": 201,
320
+ "taskInfo": base.Json{
321
+ "newCatalogID": "",
322
+ "contentInfoList": contentInfoList,
323
+ "catalogInfoList": catalogInfoList,
324
+ },
325
+ "commonAccountInfo": base.Json{
326
+ "account": d.Account,
327
+ "accountType": 1,
328
+ },
329
+ },
330
+ }
331
+ pathname := "/orchestration/personalCloud/batchOprTask/v1.0/createBatchOprTask"
332
+ if d.isFamily() {
333
+ data = base.Json{
334
+ "catalogList": catalogInfoList,
335
+ "contentList": contentInfoList,
336
+ "commonAccountInfo": base.Json{
337
+ "account": d.Account,
338
+ "accountType": 1,
339
+ },
340
+ "sourceCatalogType": 1002,
341
+ "taskType": 2,
342
+ }
343
+ pathname = "/orchestration/familyCloud/batchOprTask/v1.0/createBatchOprTask"
344
+ }
345
+ _, err := d.post(pathname, data, nil)
346
+ return err
347
+ default:
348
+ return errs.NotImplement
349
+ }
350
+ }
351
+
352
+ const (
353
+ _ = iota //ignore first value by assigning to blank identifier
354
+ KB = 1 << (10 * iota)
355
+ MB
356
+ GB
357
+ TB
358
+ )
359
+
360
+ func getPartSize(size int64) int64 {
361
+ // 网盘对于分片数量存在上限
362
+ if size/GB > 30 {
363
+ return 512 * MB
364
+ }
365
+ return 100 * MB
366
+ }
367
+
368
+ func (d *Yun139) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
369
+ switch d.Addition.Type {
370
+ case MetaPersonalNew:
371
+ var err error
372
+ fullHash := stream.GetHash().GetHash(utils.SHA256)
373
+ if len(fullHash) <= 0 {
374
+ tmpF, err := stream.CacheFullInTempFile()
375
+ if err != nil {
376
+ return err
377
+ }
378
+ fullHash, err = utils.HashFile(utils.SHA256, tmpF)
379
+ if err != nil {
380
+ return err
381
+ }
382
+ }
383
+ // return errs.NotImplement
384
+ data := base.Json{
385
+ "contentHash": fullHash,
386
+ "contentHashAlgorithm": "SHA256",
387
+ "contentType": "application/octet-stream",
388
+ "parallelUpload": false,
389
+ "partInfos": []base.Json{{
390
+ "parallelHashCtx": base.Json{
391
+ "partOffset": 0,
392
+ },
393
+ "partNumber": 1,
394
+ "partSize": stream.GetSize(),
395
+ }},
396
+ "size": stream.GetSize(),
397
+ "parentFileId": dstDir.GetID(),
398
+ "name": stream.GetName(),
399
+ "type": "file",
400
+ "fileRenameMode": "auto_rename",
401
+ }
402
+ pathname := "/hcy/file/create"
403
+ var resp PersonalUploadResp
404
+ _, err = d.personalPost(pathname, data, &resp)
405
+ if err != nil {
406
+ return err
407
+ }
408
+
409
+ if resp.Data.Exist || resp.Data.RapidUpload {
410
+ return nil
411
+ }
412
+
413
+ // Progress
414
+ p := driver.NewProgress(stream.GetSize(), up)
415
+
416
+ // Update Progress
417
+ r := io.TeeReader(stream, p)
418
+
419
+ req, err := http.NewRequest("PUT", resp.Data.PartInfos[0].UploadUrl, r)
420
+ if err != nil {
421
+ return err
422
+ }
423
+ req = req.WithContext(ctx)
424
+ req.Header.Set("Content-Type", "application/octet-stream")
425
+ req.Header.Set("Content-Length", fmt.Sprint(stream.GetSize()))
426
+ req.Header.Set("Origin", "https://yun.139.com")
427
+ req.Header.Set("Referer", "https://yun.139.com/")
428
+ req.ContentLength = stream.GetSize()
429
+
430
+ res, err := base.HttpClient.Do(req)
431
+ if err != nil {
432
+ return err
433
+ }
434
+
435
+ _ = res.Body.Close()
436
+ log.Debugf("%+v", res)
437
+ if res.StatusCode != http.StatusOK {
438
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
439
+ }
440
+
441
+ data = base.Json{
442
+ "contentHash": fullHash,
443
+ "contentHashAlgorithm": "SHA256",
444
+ "fileId": resp.Data.FileId,
445
+ "uploadId": resp.Data.UploadId,
446
+ }
447
+ _, err = d.personalPost("/hcy/file/complete", data, nil)
448
+ if err != nil {
449
+ return err
450
+ }
451
+ return nil
452
+ case MetaPersonal:
453
+ fallthrough
454
+ case MetaFamily:
455
+ data := base.Json{
456
+ "manualRename": 2,
457
+ "operation": 0,
458
+ "fileCount": 1,
459
+ "totalSize": 0, // 去除上传大小限制
460
+ "uploadContentList": []base.Json{{
461
+ "contentName": stream.GetName(),
462
+ "contentSize": 0, // 去除上传大小限制
463
+ // "digest": "5a3231986ce7a6b46e408612d385bafa"
464
+ }},
465
+ "parentCatalogID": dstDir.GetID(),
466
+ "newCatalogName": "",
467
+ "commonAccountInfo": base.Json{
468
+ "account": d.Account,
469
+ "accountType": 1,
470
+ },
471
+ }
472
+ pathname := "/orchestration/personalCloud/uploadAndDownload/v1.0/pcUploadFileRequest"
473
+ if d.isFamily() {
474
+ // data = d.newJson(base.Json{
475
+ // "fileCount": 1,
476
+ // "manualRename": 2,
477
+ // "operation": 0,
478
+ // "path": "",
479
+ // "seqNo": "",
480
+ // "totalSize": 0,
481
+ // "uploadContentList": []base.Json{{
482
+ // "contentName": stream.GetName(),
483
+ // "contentSize": 0,
484
+ // // "digest": "5a3231986ce7a6b46e408612d385bafa"
485
+ // }},
486
+ // })
487
+ // pathname = "/orchestration/familyCloud/content/v1.0/getFileUploadURL"
488
+ return errs.NotImplement
489
+ }
490
+ var resp UploadResp
491
+ _, err := d.post(pathname, data, &resp)
492
+ if err != nil {
493
+ return err
494
+ }
495
+
496
+ // Progress
497
+ p := driver.NewProgress(stream.GetSize(), up)
498
+
499
+ var partSize = getPartSize(stream.GetSize())
500
+ part := (stream.GetSize() + partSize - 1) / partSize
501
+ if part == 0 {
502
+ part = 1
503
+ }
504
+ for i := int64(0); i < part; i++ {
505
+ if utils.IsCanceled(ctx) {
506
+ return ctx.Err()
507
+ }
508
+
509
+ start := i * partSize
510
+ byteSize := stream.GetSize() - start
511
+ if byteSize > partSize {
512
+ byteSize = partSize
513
+ }
514
+
515
+ limitReader := io.LimitReader(stream, byteSize)
516
+ // Update Progress
517
+ r := io.TeeReader(limitReader, p)
518
+ req, err := http.NewRequest("POST", resp.Data.UploadResult.RedirectionURL, r)
519
+ if err != nil {
520
+ return err
521
+ }
522
+
523
+ req = req.WithContext(ctx)
524
+ req.Header.Set("Content-Type", "text/plain;name="+unicode(stream.GetName()))
525
+ req.Header.Set("contentSize", strconv.FormatInt(stream.GetSize(), 10))
526
+ req.Header.Set("range", fmt.Sprintf("bytes=%d-%d", start, start+byteSize-1))
527
+ req.Header.Set("uploadtaskID", resp.Data.UploadResult.UploadTaskID)
528
+ req.Header.Set("rangeType", "0")
529
+ req.ContentLength = byteSize
530
+
531
+ res, err := base.HttpClient.Do(req)
532
+ if err != nil {
533
+ return err
534
+ }
535
+ _ = res.Body.Close()
536
+ log.Debugf("%+v", res)
537
+ if res.StatusCode != http.StatusOK {
538
+ return fmt.Errorf("unexpected status code: %d", res.StatusCode)
539
+ }
540
+ }
541
+
542
+ return nil
543
+ default:
544
+ return errs.NotImplement
545
+ }
546
+ }
547
+
548
+ func (d *Yun139) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
549
+ switch d.Addition.Type {
550
+ case MetaPersonalNew:
551
+ var resp base.Json
552
+ var uri string
553
+ data := base.Json{
554
+ "category": "video",
555
+ "fileId": args.Obj.GetID(),
556
+ }
557
+ switch args.Method {
558
+ case "video_preview":
559
+ uri = "/hcy/videoPreview/getPreviewInfo"
560
+ default:
561
+ return nil, errs.NotSupport
562
+ }
563
+ _, err := d.personalPost(uri, data, &resp)
564
+ if err != nil {
565
+ return nil, err
566
+ }
567
+ return resp["data"], nil
568
+ default:
569
+ return nil, errs.NotImplement
570
+ }
571
+ }
572
+
573
+ var _ driver.Driver = (*Yun139)(nil)
drivers/139/meta.go ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
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
+ //Account string `json:"account" required:"true"`
10
+ Authorization string `json:"authorization" type:"text" required:"true"`
11
+ driver.RootID
12
+ Type string `json:"type" type:"select" options:"personal,family,personal_new" default:"personal"`
13
+ CloudID string `json:"cloud_id"`
14
+ }
15
+
16
+ var config = driver.Config{
17
+ Name: "139Yun",
18
+ LocalSort: true,
19
+ }
20
+
21
+ func init() {
22
+ op.RegisterDriver(func() driver.Driver {
23
+ return &Yun139{}
24
+ })
25
+ }
drivers/139/types.go ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "encoding/xml"
5
+ )
6
+
7
+ const (
8
+ MetaPersonal string = "personal"
9
+ MetaFamily string = "family"
10
+ MetaPersonalNew string = "personal_new"
11
+ )
12
+
13
+ type BaseResp struct {
14
+ Success bool `json:"success"`
15
+ Code string `json:"code"`
16
+ Message string `json:"message"`
17
+ }
18
+
19
+ type Catalog struct {
20
+ CatalogID string `json:"catalogID"`
21
+ CatalogName string `json:"catalogName"`
22
+ //CatalogType int `json:"catalogType"`
23
+ CreateTime string `json:"createTime"`
24
+ UpdateTime string `json:"updateTime"`
25
+ //IsShared bool `json:"isShared"`
26
+ //CatalogLevel int `json:"catalogLevel"`
27
+ //ShareDoneeCount int `json:"shareDoneeCount"`
28
+ //OpenType int `json:"openType"`
29
+ //ParentCatalogID string `json:"parentCatalogId"`
30
+ //DirEtag int `json:"dirEtag"`
31
+ //Tombstoned int `json:"tombstoned"`
32
+ //ProxyID interface{} `json:"proxyID"`
33
+ //Moved int `json:"moved"`
34
+ //IsFixedDir int `json:"isFixedDir"`
35
+ //IsSynced interface{} `json:"isSynced"`
36
+ //Owner string `json:"owner"`
37
+ //Modifier interface{} `json:"modifier"`
38
+ //Path string `json:"path"`
39
+ //ShareType int `json:"shareType"`
40
+ //SoftLink interface{} `json:"softLink"`
41
+ //ExtProp1 interface{} `json:"extProp1"`
42
+ //ExtProp2 interface{} `json:"extProp2"`
43
+ //ExtProp3 interface{} `json:"extProp3"`
44
+ //ExtProp4 interface{} `json:"extProp4"`
45
+ //ExtProp5 interface{} `json:"extProp5"`
46
+ //ETagOprType int `json:"ETagOprType"`
47
+ }
48
+
49
+ type Content struct {
50
+ ContentID string `json:"contentID"`
51
+ ContentName string `json:"contentName"`
52
+ //ContentSuffix string `json:"contentSuffix"`
53
+ ContentSize int64 `json:"contentSize"`
54
+ //ContentDesc string `json:"contentDesc"`
55
+ //ContentType int `json:"contentType"`
56
+ //ContentOrigin int `json:"contentOrigin"`
57
+ UpdateTime string `json:"updateTime"`
58
+ //CommentCount int `json:"commentCount"`
59
+ ThumbnailURL string `json:"thumbnailURL"`
60
+ //BigthumbnailURL string `json:"bigthumbnailURL"`
61
+ //PresentURL string `json:"presentURL"`
62
+ //PresentLURL string `json:"presentLURL"`
63
+ //PresentHURL string `json:"presentHURL"`
64
+ //ContentTAGList interface{} `json:"contentTAGList"`
65
+ //ShareDoneeCount int `json:"shareDoneeCount"`
66
+ //Safestate int `json:"safestate"`
67
+ //Transferstate int `json:"transferstate"`
68
+ //IsFocusContent int `json:"isFocusContent"`
69
+ //UpdateShareTime interface{} `json:"updateShareTime"`
70
+ //UploadTime string `json:"uploadTime"`
71
+ //OpenType int `json:"openType"`
72
+ //AuditResult int `json:"auditResult"`
73
+ //ParentCatalogID string `json:"parentCatalogId"`
74
+ //Channel string `json:"channel"`
75
+ //GeoLocFlag string `json:"geoLocFlag"`
76
+ Digest string `json:"digest"`
77
+ //Version string `json:"version"`
78
+ //FileEtag string `json:"fileEtag"`
79
+ //FileVersion string `json:"fileVersion"`
80
+ //Tombstoned int `json:"tombstoned"`
81
+ //ProxyID string `json:"proxyID"`
82
+ //Moved int `json:"moved"`
83
+ //MidthumbnailURL string `json:"midthumbnailURL"`
84
+ //Owner string `json:"owner"`
85
+ //Modifier string `json:"modifier"`
86
+ //ShareType int `json:"shareType"`
87
+ //ExtInfo struct {
88
+ // Uploader string `json:"uploader"`
89
+ // Address string `json:"address"`
90
+ //} `json:"extInfo"`
91
+ //Exif struct {
92
+ // CreateTime string `json:"createTime"`
93
+ // Longitude interface{} `json:"longitude"`
94
+ // Latitude interface{} `json:"latitude"`
95
+ // LocalSaveTime interface{} `json:"localSaveTime"`
96
+ //} `json:"exif"`
97
+ //CollectionFlag interface{} `json:"collectionFlag"`
98
+ //TreeInfo interface{} `json:"treeInfo"`
99
+ //IsShared bool `json:"isShared"`
100
+ //ETagOprType int `json:"ETagOprType"`
101
+ }
102
+
103
+ type GetDiskResp struct {
104
+ BaseResp
105
+ Data struct {
106
+ Result struct {
107
+ ResultCode string `json:"resultCode"`
108
+ ResultDesc interface{} `json:"resultDesc"`
109
+ } `json:"result"`
110
+ GetDiskResult struct {
111
+ ParentCatalogID string `json:"parentCatalogID"`
112
+ NodeCount int `json:"nodeCount"`
113
+ CatalogList []Catalog `json:"catalogList"`
114
+ ContentList []Content `json:"contentList"`
115
+ IsCompleted int `json:"isCompleted"`
116
+ } `json:"getDiskResult"`
117
+ } `json:"data"`
118
+ }
119
+
120
+ type UploadResp struct {
121
+ BaseResp
122
+ Data struct {
123
+ Result struct {
124
+ ResultCode string `json:"resultCode"`
125
+ ResultDesc interface{} `json:"resultDesc"`
126
+ } `json:"result"`
127
+ UploadResult struct {
128
+ UploadTaskID string `json:"uploadTaskID"`
129
+ RedirectionURL string `json:"redirectionUrl"`
130
+ NewContentIDList []struct {
131
+ ContentID string `json:"contentID"`
132
+ ContentName string `json:"contentName"`
133
+ IsNeedUpload string `json:"isNeedUpload"`
134
+ FileEtag int64 `json:"fileEtag"`
135
+ FileVersion int64 `json:"fileVersion"`
136
+ OverridenFlag int `json:"overridenFlag"`
137
+ } `json:"newContentIDList"`
138
+ CatalogIDList interface{} `json:"catalogIDList"`
139
+ IsSlice interface{} `json:"isSlice"`
140
+ } `json:"uploadResult"`
141
+ } `json:"data"`
142
+ }
143
+
144
+ type CloudContent struct {
145
+ ContentID string `json:"contentID"`
146
+ //Modifier string `json:"modifier"`
147
+ //Nickname string `json:"nickname"`
148
+ //CloudNickName string `json:"cloudNickName"`
149
+ ContentName string `json:"contentName"`
150
+ //ContentType int `json:"contentType"`
151
+ //ContentSuffix string `json:"contentSuffix"`
152
+ ContentSize int64 `json:"contentSize"`
153
+ //ContentDesc string `json:"contentDesc"`
154
+ CreateTime string `json:"createTime"`
155
+ //Shottime interface{} `json:"shottime"`
156
+ LastUpdateTime string `json:"lastUpdateTime"`
157
+ ThumbnailURL string `json:"thumbnailURL"`
158
+ //MidthumbnailURL string `json:"midthumbnailURL"`
159
+ //BigthumbnailURL string `json:"bigthumbnailURL"`
160
+ //PresentURL string `json:"presentURL"`
161
+ //PresentLURL string `json:"presentLURL"`
162
+ //PresentHURL string `json:"presentHURL"`
163
+ //ParentCatalogID string `json:"parentCatalogID"`
164
+ //Uploader string `json:"uploader"`
165
+ //UploaderNickName string `json:"uploaderNickName"`
166
+ //TreeInfo interface{} `json:"treeInfo"`
167
+ //UpdateTime interface{} `json:"updateTime"`
168
+ //ExtInfo struct {
169
+ // Uploader string `json:"uploader"`
170
+ //} `json:"extInfo"`
171
+ //EtagOprType interface{} `json:"etagOprType"`
172
+ }
173
+
174
+ type CloudCatalog struct {
175
+ CatalogID string `json:"catalogID"`
176
+ CatalogName string `json:"catalogName"`
177
+ //CloudID string `json:"cloudID"`
178
+ CreateTime string `json:"createTime"`
179
+ LastUpdateTime string `json:"lastUpdateTime"`
180
+ //Creator string `json:"creator"`
181
+ //CreatorNickname string `json:"creatorNickname"`
182
+ }
183
+
184
+ type QueryContentListResp struct {
185
+ BaseResp
186
+ Data struct {
187
+ Result struct {
188
+ ResultCode string `json:"resultCode"`
189
+ ResultDesc string `json:"resultDesc"`
190
+ } `json:"result"`
191
+ Path string `json:"path"`
192
+ CloudContentList []CloudContent `json:"cloudContentList"`
193
+ CloudCatalogList []CloudCatalog `json:"cloudCatalogList"`
194
+ TotalCount int `json:"totalCount"`
195
+ RecallContent interface{} `json:"recallContent"`
196
+ } `json:"data"`
197
+ }
198
+
199
+ type PersonalThumbnail struct {
200
+ Style string `json:"style"`
201
+ Url string `json:"url"`
202
+ }
203
+
204
+ type PersonalFileItem struct {
205
+ FileId string `json:"fileId"`
206
+ Name string `json:"name"`
207
+ Size int64 `json:"size"`
208
+ Type string `json:"type"`
209
+ CreatedAt string `json:"createdAt"`
210
+ UpdatedAt string `json:"updatedAt"`
211
+ Thumbnails []PersonalThumbnail `json:"thumbnailUrls"`
212
+ }
213
+
214
+ type PersonalListResp struct {
215
+ BaseResp
216
+ Data struct {
217
+ Items []PersonalFileItem `json:"items"`
218
+ NextPageCursor string `json:"nextPageCursor"`
219
+ }
220
+ }
221
+
222
+ type PersonalPartInfo struct {
223
+ PartNumber int `json:"partNumber"`
224
+ UploadUrl string `json:"uploadUrl"`
225
+ }
226
+
227
+ type PersonalUploadResp struct {
228
+ BaseResp
229
+ Data struct {
230
+ FileId string `json:"fileId"`
231
+ PartInfos []PersonalPartInfo `json:"partInfos"`
232
+ Exist bool `json:"exist"`
233
+ RapidUpload bool `json:"rapidUpload"`
234
+ UploadId string `json:"uploadId"`
235
+ }
236
+ }
237
+
238
+ type RefreshTokenResp struct {
239
+ XMLName xml.Name `xml:"root"`
240
+ Return string `xml:"return"`
241
+ Token string `xml:"token"`
242
+ Expiretime int32 `xml:"expiretime"`
243
+ AccessToken string `xml:"accessToken"`
244
+ Desc string `xml:"desc"`
245
+ }
drivers/139/util.go ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _139
2
+
3
+ import (
4
+ "encoding/base64"
5
+ "errors"
6
+ "fmt"
7
+ "net/http"
8
+ "net/url"
9
+ "sort"
10
+ "strconv"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/alist-org/alist/v3/drivers/base"
15
+ "github.com/alist-org/alist/v3/internal/model"
16
+ "github.com/alist-org/alist/v3/pkg/utils"
17
+ "github.com/alist-org/alist/v3/pkg/utils/random"
18
+ "github.com/alist-org/alist/v3/internal/op"
19
+ "github.com/go-resty/resty/v2"
20
+ jsoniter "github.com/json-iterator/go"
21
+ log "github.com/sirupsen/logrus"
22
+ )
23
+
24
+ // do others that not defined in Driver interface
25
+ func (d *Yun139) isFamily() bool {
26
+ return d.Type == "family"
27
+ }
28
+
29
+ func encodeURIComponent(str string) string {
30
+ r := url.QueryEscape(str)
31
+ r = strings.Replace(r, "+", "%20", -1)
32
+ r = strings.Replace(r, "%21", "!", -1)
33
+ r = strings.Replace(r, "%27", "'", -1)
34
+ r = strings.Replace(r, "%28", "(", -1)
35
+ r = strings.Replace(r, "%29", ")", -1)
36
+ r = strings.Replace(r, "%2A", "*", -1)
37
+ return r
38
+ }
39
+
40
+ func calSign(body, ts, randStr string) string {
41
+ body = encodeURIComponent(body)
42
+ strs := strings.Split(body, "")
43
+ sort.Strings(strs)
44
+ body = strings.Join(strs, "")
45
+ body = base64.StdEncoding.EncodeToString([]byte(body))
46
+ res := utils.GetMD5EncodeStr(body) + utils.GetMD5EncodeStr(ts+":"+randStr)
47
+ res = strings.ToUpper(utils.GetMD5EncodeStr(res))
48
+ return res
49
+ }
50
+
51
+ func getTime(t string) time.Time {
52
+ stamp, _ := time.ParseInLocation("20060102150405", t, utils.CNLoc)
53
+ return stamp
54
+ }
55
+
56
+ func (d *Yun139) refreshToken() error {
57
+ url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
58
+ var resp RefreshTokenResp
59
+ decode, err := base64.StdEncoding.DecodeString(d.Authorization)
60
+ if err != nil {
61
+ return err
62
+ }
63
+ decodeStr := string(decode)
64
+ splits := strings.Split(decodeStr, ":")
65
+ reqBody := "<root><token>" + splits[2] + "</token><account>" + splits[1] + "</account><clienttype>656</clienttype></root>"
66
+ _, err = base.RestyClient.R().
67
+ ForceContentType("application/xml").
68
+ SetBody(reqBody).
69
+ SetResult(&resp).
70
+ Post(url)
71
+ if err != nil {
72
+ return err
73
+ }
74
+ if resp.Return != "0" {
75
+ return fmt.Errorf("failed to refresh token: %s", resp.Desc)
76
+ }
77
+ d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
78
+ op.MustSaveDriverStorage(d)
79
+ return nil
80
+ }
81
+
82
+ func (d *Yun139) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
83
+ url := "https://yun.139.com" + pathname
84
+ req := base.RestyClient.R()
85
+ randStr := random.String(16)
86
+ ts := time.Now().Format("2006-01-02 15:04:05")
87
+ if callback != nil {
88
+ callback(req)
89
+ }
90
+ body, err := utils.Json.Marshal(req.Body)
91
+ if err != nil {
92
+ return nil, err
93
+ }
94
+ sign := calSign(string(body), ts, randStr)
95
+ svcType := "1"
96
+ if d.isFamily() {
97
+ svcType = "2"
98
+ }
99
+ req.SetHeaders(map[string]string{
100
+ "Accept": "application/json, text/plain, */*",
101
+ "CMS-DEVICE": "default",
102
+ "Authorization": "Basic " + d.Authorization,
103
+ "mcloud-channel": "1000101",
104
+ "mcloud-client": "10701",
105
+ //"mcloud-route": "001",
106
+ "mcloud-sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
107
+ //"mcloud-skey":"",
108
+ "mcloud-version": "6.6.0",
109
+ "Origin": "https://yun.139.com",
110
+ "Referer": "https://yun.139.com/w/",
111
+ "x-DeviceInfo": "||9|6.6.0|chrome|95.0.4638.69|uwIy75obnsRPIwlJSd7D9GhUvFwG96ce||macos 10.15.2||zh-CN|||",
112
+ "x-huawei-channelSrc": "10000034",
113
+ "x-inner-ntwk": "2",
114
+ "x-m4c-caller": "PC",
115
+ "x-m4c-src": "10002",
116
+ "x-SvcType": svcType,
117
+ })
118
+
119
+ var e BaseResp
120
+ req.SetResult(&e)
121
+ res, err := req.Execute(method, url)
122
+ log.Debugln(res.String())
123
+ if !e.Success {
124
+ return nil, errors.New(e.Message)
125
+ }
126
+ if resp != nil {
127
+ err = utils.Json.Unmarshal(res.Body(), resp)
128
+ if err != nil {
129
+ return nil, err
130
+ }
131
+ }
132
+ return res.Body(), nil
133
+ }
134
+ func (d *Yun139) post(pathname string, data interface{}, resp interface{}) ([]byte, error) {
135
+ return d.request(pathname, http.MethodPost, func(req *resty.Request) {
136
+ req.SetBody(data)
137
+ }, resp)
138
+ }
139
+
140
+ func (d *Yun139) getFiles(catalogID string) ([]model.Obj, error) {
141
+ start := 0
142
+ limit := 100
143
+ files := make([]model.Obj, 0)
144
+ for {
145
+ data := base.Json{
146
+ "catalogID": catalogID,
147
+ "sortDirection": 1,
148
+ "startNumber": start + 1,
149
+ "endNumber": start + limit,
150
+ "filterType": 0,
151
+ "catalogSortType": 0,
152
+ "contentSortType": 0,
153
+ "commonAccountInfo": base.Json{
154
+ "account": d.Account,
155
+ "accountType": 1,
156
+ },
157
+ }
158
+ var resp GetDiskResp
159
+ _, err := d.post("/orchestration/personalCloud/catalog/v1.0/getDisk", data, &resp)
160
+ if err != nil {
161
+ return nil, err
162
+ }
163
+ for _, catalog := range resp.Data.GetDiskResult.CatalogList {
164
+ f := model.Object{
165
+ ID: catalog.CatalogID,
166
+ Name: catalog.CatalogName,
167
+ Size: 0,
168
+ Modified: getTime(catalog.UpdateTime),
169
+ Ctime: getTime(catalog.CreateTime),
170
+ IsFolder: true,
171
+ }
172
+ files = append(files, &f)
173
+ }
174
+ for _, content := range resp.Data.GetDiskResult.ContentList {
175
+ f := model.ObjThumb{
176
+ Object: model.Object{
177
+ ID: content.ContentID,
178
+ Name: content.ContentName,
179
+ Size: content.ContentSize,
180
+ Modified: getTime(content.UpdateTime),
181
+ HashInfo: utils.NewHashInfo(utils.MD5, content.Digest),
182
+ },
183
+ Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
184
+ //Thumbnail: content.BigthumbnailURL,
185
+ }
186
+ files = append(files, &f)
187
+ }
188
+ if start+limit >= resp.Data.GetDiskResult.NodeCount {
189
+ break
190
+ }
191
+ start += limit
192
+ }
193
+ return files, nil
194
+ }
195
+
196
+ func (d *Yun139) newJson(data map[string]interface{}) base.Json {
197
+ common := map[string]interface{}{
198
+ "catalogType": 3,
199
+ "cloudID": d.CloudID,
200
+ "cloudType": 1,
201
+ "commonAccountInfo": base.Json{
202
+ "account": d.Account,
203
+ "accountType": 1,
204
+ },
205
+ }
206
+ return utils.MergeMap(data, common)
207
+ }
208
+
209
+ func (d *Yun139) familyGetFiles(catalogID string) ([]model.Obj, error) {
210
+ pageNum := 1
211
+ files := make([]model.Obj, 0)
212
+ for {
213
+ data := d.newJson(base.Json{
214
+ "catalogID": catalogID,
215
+ "contentSortType": 0,
216
+ "pageInfo": base.Json{
217
+ "pageNum": pageNum,
218
+ "pageSize": 100,
219
+ },
220
+ "sortDirection": 1,
221
+ })
222
+ var resp QueryContentListResp
223
+ _, err := d.post("/orchestration/familyCloud/content/v1.0/queryContentList", data, &resp)
224
+ if err != nil {
225
+ return nil, err
226
+ }
227
+ for _, catalog := range resp.Data.CloudCatalogList {
228
+ f := model.Object{
229
+ ID: catalog.CatalogID,
230
+ Name: catalog.CatalogName,
231
+ Size: 0,
232
+ IsFolder: true,
233
+ Modified: getTime(catalog.LastUpdateTime),
234
+ Ctime: getTime(catalog.CreateTime),
235
+ }
236
+ files = append(files, &f)
237
+ }
238
+ for _, content := range resp.Data.CloudContentList {
239
+ f := model.ObjThumb{
240
+ Object: model.Object{
241
+ ID: content.ContentID,
242
+ Name: content.ContentName,
243
+ Size: content.ContentSize,
244
+ Modified: getTime(content.LastUpdateTime),
245
+ Ctime: getTime(content.CreateTime),
246
+ },
247
+ Thumbnail: model.Thumbnail{Thumbnail: content.ThumbnailURL},
248
+ //Thumbnail: content.BigthumbnailURL,
249
+ }
250
+ files = append(files, &f)
251
+ }
252
+ if 100*pageNum > resp.Data.TotalCount {
253
+ break
254
+ }
255
+ pageNum++
256
+ }
257
+ return files, nil
258
+ }
259
+
260
+ func (d *Yun139) getLink(contentId string) (string, error) {
261
+ data := base.Json{
262
+ "appName": "",
263
+ "contentID": contentId,
264
+ "commonAccountInfo": base.Json{
265
+ "account": d.Account,
266
+ "accountType": 1,
267
+ },
268
+ }
269
+ res, err := d.post("/orchestration/personalCloud/uploadAndDownload/v1.0/downloadRequest",
270
+ data, nil)
271
+ if err != nil {
272
+ return "", err
273
+ }
274
+ return jsoniter.Get(res, "data", "downloadURL").ToString(), nil
275
+ }
276
+
277
+ func unicode(str string) string {
278
+ textQuoted := strconv.QuoteToASCII(str)
279
+ textUnquoted := textQuoted[1 : len(textQuoted)-1]
280
+ return textUnquoted
281
+ }
282
+
283
+ func (d *Yun139) personalRequest(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
284
+ url := "https://personal-kd-njs.yun.139.com" + pathname
285
+ req := base.RestyClient.R()
286
+ randStr := random.String(16)
287
+ ts := time.Now().Format("2006-01-02 15:04:05")
288
+ if callback != nil {
289
+ callback(req)
290
+ }
291
+ body, err := utils.Json.Marshal(req.Body)
292
+ if err != nil {
293
+ return nil, err
294
+ }
295
+ sign := calSign(string(body), ts, randStr)
296
+ svcType := "1"
297
+ if d.isFamily() {
298
+ svcType = "2"
299
+ }
300
+ req.SetHeaders(map[string]string{
301
+ "Accept": "application/json, text/plain, */*",
302
+ "Authorization": "Basic " + d.Authorization,
303
+ "Caller": "web",
304
+ "Cms-Device": "default",
305
+ "Mcloud-Channel": "1000101",
306
+ "Mcloud-Client": "10701",
307
+ "Mcloud-Route": "001",
308
+ "Mcloud-Sign": fmt.Sprintf("%s,%s,%s", ts, randStr, sign),
309
+ "Mcloud-Version": "7.13.0",
310
+ "Origin": "https://yun.139.com",
311
+ "Referer": "https://yun.139.com/w/",
312
+ "x-DeviceInfo": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||",
313
+ "x-huawei-channelSrc": "10000034",
314
+ "x-inner-ntwk": "2",
315
+ "x-m4c-caller": "PC",
316
+ "x-m4c-src": "10002",
317
+ "x-SvcType": svcType,
318
+ "X-Yun-Api-Version": "v1",
319
+ "X-Yun-App-Channel": "10000034",
320
+ "X-Yun-Channel-Source": "10000034",
321
+ "X-Yun-Client-Info": "||9|7.13.0|chrome|120.0.0.0|||windows 10||zh-CN|||dW5kZWZpbmVk||",
322
+ "X-Yun-Module-Type": "100",
323
+ "X-Yun-Svc-Type": "1",
324
+ })
325
+
326
+ var e BaseResp
327
+ req.SetResult(&e)
328
+ res, err := req.Execute(method, url)
329
+ if err != nil {
330
+ return nil, err
331
+ }
332
+ log.Debugln(res.String())
333
+ if !e.Success {
334
+ return nil, errors.New(e.Message)
335
+ }
336
+ if resp != nil {
337
+ err = utils.Json.Unmarshal(res.Body(), resp)
338
+ if err != nil {
339
+ return nil, err
340
+ }
341
+ }
342
+ return res.Body(), nil
343
+ }
344
+ func (d *Yun139) personalPost(pathname string, data interface{}, resp interface{}) ([]byte, error) {
345
+ return d.personalRequest(pathname, http.MethodPost, func(req *resty.Request) {
346
+ req.SetBody(data)
347
+ }, resp)
348
+ }
349
+
350
+ func getPersonalTime(t string) time.Time {
351
+ stamp, err := time.ParseInLocation("2006-01-02T15:04:05.999-07:00", t, utils.CNLoc)
352
+ if err != nil {
353
+ panic(err)
354
+ }
355
+ return stamp
356
+ }
357
+
358
+ func (d *Yun139) personalGetFiles(fileId string) ([]model.Obj, error) {
359
+ files := make([]model.Obj, 0)
360
+ nextPageCursor := ""
361
+ for {
362
+ data := base.Json{
363
+ "imageThumbnailStyleList": []string{"Small", "Large"},
364
+ "orderBy": "updated_at",
365
+ "orderDirection": "DESC",
366
+ "pageInfo": base.Json{
367
+ "pageCursor": nextPageCursor,
368
+ "pageSize": 100,
369
+ },
370
+ "parentFileId": fileId,
371
+ }
372
+ var resp PersonalListResp
373
+ _, err := d.personalPost("/hcy/file/list", data, &resp)
374
+ if err != nil {
375
+ return nil, err
376
+ }
377
+ nextPageCursor = resp.Data.NextPageCursor
378
+ for _, item := range resp.Data.Items {
379
+ var isFolder = (item.Type == "folder")
380
+ var f model.Obj
381
+ if isFolder {
382
+ f = &model.Object{
383
+ ID: item.FileId,
384
+ Name: item.Name,
385
+ Size: 0,
386
+ Modified: getPersonalTime(item.UpdatedAt),
387
+ Ctime: getPersonalTime(item.CreatedAt),
388
+ IsFolder: isFolder,
389
+ }
390
+ } else {
391
+ var Thumbnails = item.Thumbnails
392
+ var ThumbnailUrl string
393
+ if len(Thumbnails) > 0 {
394
+ ThumbnailUrl = Thumbnails[len(Thumbnails)-1].Url
395
+ }
396
+ f = &model.ObjThumb{
397
+ Object: model.Object{
398
+ ID: item.FileId,
399
+ Name: item.Name,
400
+ Size: item.Size,
401
+ Modified: getPersonalTime(item.UpdatedAt),
402
+ Ctime: getPersonalTime(item.CreatedAt),
403
+ IsFolder: isFolder,
404
+ },
405
+ Thumbnail: model.Thumbnail{Thumbnail: ThumbnailUrl},
406
+ }
407
+ }
408
+ files = append(files, f)
409
+ }
410
+ if len(nextPageCursor) == 0 {
411
+ break
412
+ }
413
+ }
414
+ return files, nil
415
+ }
416
+
417
+ func (d *Yun139) personalGetLink(fileId string) (string, error) {
418
+ data := base.Json{
419
+ "fileId": fileId,
420
+ }
421
+ res, err := d.personalPost("/hcy/file/getDownloadUrl",
422
+ data, nil)
423
+ if err != nil {
424
+ return "", err
425
+ }
426
+ var cdnUrl = jsoniter.Get(res, "data", "cdnUrl").ToString()
427
+ if cdnUrl != "" {
428
+ return cdnUrl, nil
429
+ } else {
430
+ return jsoniter.Get(res, "data", "url").ToString(), nil
431
+ }
432
+ }
drivers/189/driver.go ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "strings"
7
+
8
+ "github.com/alist-org/alist/v3/drivers/base"
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/utils"
12
+ "github.com/go-resty/resty/v2"
13
+ log "github.com/sirupsen/logrus"
14
+ )
15
+
16
+ type Cloud189 struct {
17
+ model.Storage
18
+ Addition
19
+ client *resty.Client
20
+ rsa Rsa
21
+ sessionKey string
22
+ }
23
+
24
+ func (d *Cloud189) Config() driver.Config {
25
+ return config
26
+ }
27
+
28
+ func (d *Cloud189) GetAddition() driver.Additional {
29
+ return &d.Addition
30
+ }
31
+
32
+ func (d *Cloud189) Init(ctx context.Context) error {
33
+ d.client = base.NewRestyClient().
34
+ SetHeader("Referer", "https://cloud.189.cn/")
35
+ return d.newLogin()
36
+ }
37
+
38
+ func (d *Cloud189) Drop(ctx context.Context) error {
39
+ return nil
40
+ }
41
+
42
+ func (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
43
+ return d.getFiles(dir.GetID())
44
+ }
45
+
46
+ func (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
47
+ var resp DownResp
48
+ u := "https://cloud.189.cn/api/portal/getFileInfo.action"
49
+ _, err := d.request(u, http.MethodGet, func(req *resty.Request) {
50
+ req.SetQueryParam("fileId", file.GetID())
51
+ }, &resp)
52
+ if err != nil {
53
+ return nil, err
54
+ }
55
+ client := resty.NewWithClient(d.client.GetClient()).SetRedirectPolicy(
56
+ resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
57
+ return http.ErrUseLastResponse
58
+ }))
59
+ res, err := client.R().SetHeader("User-Agent", base.UserAgent).Get("https:" + resp.FileDownloadUrl)
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+ log.Debugln(res.Status())
64
+ log.Debugln(res.String())
65
+ link := model.Link{}
66
+ log.Debugln("first url:", resp.FileDownloadUrl)
67
+ if res.StatusCode() == 302 {
68
+ link.URL = res.Header().Get("location")
69
+ log.Debugln("second url:", link.URL)
70
+ _, _ = client.R().Get(link.URL)
71
+ if res.StatusCode() == 302 {
72
+ link.URL = res.Header().Get("location")
73
+ }
74
+ log.Debugln("third url:", link.URL)
75
+ } else {
76
+ link.URL = resp.FileDownloadUrl
77
+ }
78
+ link.URL = strings.Replace(link.URL, "http://", "https://", 1)
79
+ return &link, nil
80
+ }
81
+
82
+ func (d *Cloud189) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
83
+ form := map[string]string{
84
+ "parentFolderId": parentDir.GetID(),
85
+ "folderName": dirName,
86
+ }
87
+ _, err := d.request("https://cloud.189.cn/api/open/file/createFolder.action", http.MethodPost, func(req *resty.Request) {
88
+ req.SetFormData(form)
89
+ }, nil)
90
+ return err
91
+ }
92
+
93
+ func (d *Cloud189) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
94
+ isFolder := 0
95
+ if srcObj.IsDir() {
96
+ isFolder = 1
97
+ }
98
+ taskInfos := []base.Json{
99
+ {
100
+ "fileId": srcObj.GetID(),
101
+ "fileName": srcObj.GetName(),
102
+ "isFolder": isFolder,
103
+ },
104
+ }
105
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
106
+ if err != nil {
107
+ return err
108
+ }
109
+ form := map[string]string{
110
+ "type": "MOVE",
111
+ "targetFolderId": dstDir.GetID(),
112
+ "taskInfos": string(taskInfosBytes),
113
+ }
114
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
115
+ req.SetFormData(form)
116
+ }, nil)
117
+ return err
118
+ }
119
+
120
+ func (d *Cloud189) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
121
+ url := "https://cloud.189.cn/api/open/file/renameFile.action"
122
+ idKey := "fileId"
123
+ nameKey := "destFileName"
124
+ if srcObj.IsDir() {
125
+ url = "https://cloud.189.cn/api/open/file/renameFolder.action"
126
+ idKey = "folderId"
127
+ nameKey = "destFolderName"
128
+ }
129
+ form := map[string]string{
130
+ idKey: srcObj.GetID(),
131
+ nameKey: newName,
132
+ }
133
+ _, err := d.request(url, http.MethodPost, func(req *resty.Request) {
134
+ req.SetFormData(form)
135
+ }, nil)
136
+ return err
137
+ }
138
+
139
+ func (d *Cloud189) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
140
+ isFolder := 0
141
+ if srcObj.IsDir() {
142
+ isFolder = 1
143
+ }
144
+ taskInfos := []base.Json{
145
+ {
146
+ "fileId": srcObj.GetID(),
147
+ "fileName": srcObj.GetName(),
148
+ "isFolder": isFolder,
149
+ },
150
+ }
151
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
152
+ if err != nil {
153
+ return err
154
+ }
155
+ form := map[string]string{
156
+ "type": "COPY",
157
+ "targetFolderId": dstDir.GetID(),
158
+ "taskInfos": string(taskInfosBytes),
159
+ }
160
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
161
+ req.SetFormData(form)
162
+ }, nil)
163
+ return err
164
+ }
165
+
166
+ func (d *Cloud189) Remove(ctx context.Context, obj model.Obj) error {
167
+ isFolder := 0
168
+ if obj.IsDir() {
169
+ isFolder = 1
170
+ }
171
+ taskInfos := []base.Json{
172
+ {
173
+ "fileId": obj.GetID(),
174
+ "fileName": obj.GetName(),
175
+ "isFolder": isFolder,
176
+ },
177
+ }
178
+ taskInfosBytes, err := utils.Json.Marshal(taskInfos)
179
+ if err != nil {
180
+ return err
181
+ }
182
+ form := map[string]string{
183
+ "type": "DELETE",
184
+ "targetFolderId": "",
185
+ "taskInfos": string(taskInfosBytes),
186
+ }
187
+ _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) {
188
+ req.SetFormData(form)
189
+ }, nil)
190
+ return err
191
+ }
192
+
193
+ func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
194
+ return d.newUpload(ctx, dstDir, stream, up)
195
+ }
196
+
197
+ var _ driver.Driver = (*Cloud189)(nil)
drivers/189/help.go ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/aes"
6
+ "crypto/hmac"
7
+ "crypto/md5"
8
+ "crypto/rand"
9
+ "crypto/rsa"
10
+ "crypto/sha1"
11
+ "crypto/x509"
12
+ "encoding/base64"
13
+ "encoding/hex"
14
+ "encoding/pem"
15
+ "fmt"
16
+ "net/url"
17
+ "regexp"
18
+ "strconv"
19
+ "strings"
20
+
21
+ myrand "github.com/alist-org/alist/v3/pkg/utils/random"
22
+ log "github.com/sirupsen/logrus"
23
+ )
24
+
25
+ func random() string {
26
+ return fmt.Sprintf("0.%17v", myrand.Rand.Int63n(100000000000000000))
27
+ }
28
+
29
+ func RsaEncode(origData []byte, j_rsakey string, hex bool) string {
30
+ publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----")
31
+ block, _ := pem.Decode(publicKey)
32
+ pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes)
33
+ pub := pubInterface.(*rsa.PublicKey)
34
+ b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData)
35
+ if err != nil {
36
+ log.Errorf("err: %s", err.Error())
37
+ }
38
+ res := base64.StdEncoding.EncodeToString(b)
39
+ if hex {
40
+ return b64tohex(res)
41
+ }
42
+ return res
43
+ }
44
+
45
+ var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
46
+
47
+ var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz"
48
+
49
+ func int2char(a int) string {
50
+ return strings.Split(BI_RM, "")[a]
51
+ }
52
+
53
+ func b64tohex(a string) string {
54
+ d := ""
55
+ e := 0
56
+ c := 0
57
+ for i := 0; i < len(a); i++ {
58
+ m := strings.Split(a, "")[i]
59
+ if m != "=" {
60
+ v := strings.Index(b64map, m)
61
+ if 0 == e {
62
+ e = 1
63
+ d += int2char(v >> 2)
64
+ c = 3 & v
65
+ } else if 1 == e {
66
+ e = 2
67
+ d += int2char(c<<2 | v>>4)
68
+ c = 15 & v
69
+ } else if 2 == e {
70
+ e = 3
71
+ d += int2char(c)
72
+ d += int2char(v >> 2)
73
+ c = 3 & v
74
+ } else {
75
+ e = 0
76
+ d += int2char(c<<2 | v>>4)
77
+ d += int2char(15 & v)
78
+ }
79
+ }
80
+ }
81
+ if e == 1 {
82
+ d += int2char(c << 2)
83
+ }
84
+ return d
85
+ }
86
+
87
+ func qs(form map[string]string) string {
88
+ f := make(url.Values)
89
+ for k, v := range form {
90
+ f.Set(k, v)
91
+ }
92
+ return EncodeParam(f)
93
+ //strList := make([]string, 0)
94
+ //for k, v := range form {
95
+ // strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v)))
96
+ //}
97
+ //return strings.Join(strList, "&")
98
+ }
99
+
100
+ func EncodeParam(v url.Values) string {
101
+ if v == nil {
102
+ return ""
103
+ }
104
+ var buf strings.Builder
105
+ keys := make([]string, 0, len(v))
106
+ for k := range v {
107
+ keys = append(keys, k)
108
+ }
109
+ for _, k := range keys {
110
+ vs := v[k]
111
+ for _, v := range vs {
112
+ if buf.Len() > 0 {
113
+ buf.WriteByte('&')
114
+ }
115
+ buf.WriteString(k)
116
+ buf.WriteByte('=')
117
+ //if k == "fileName" {
118
+ // buf.WriteString(encode(v))
119
+ //} else {
120
+ buf.WriteString(v)
121
+ //}
122
+ }
123
+ }
124
+ return buf.String()
125
+ }
126
+
127
+ func encode(str string) string {
128
+ //str = strings.ReplaceAll(str, "%", "%25")
129
+ //str = strings.ReplaceAll(str, "&", "%26")
130
+ //str = strings.ReplaceAll(str, "+", "%2B")
131
+ //return str
132
+ return url.QueryEscape(str)
133
+ }
134
+
135
+ func AesEncrypt(data, key []byte) []byte {
136
+ block, _ := aes.NewCipher(key)
137
+ if block == nil {
138
+ return []byte{}
139
+ }
140
+ data = PKCS7Padding(data, block.BlockSize())
141
+ decrypted := make([]byte, len(data))
142
+ size := block.BlockSize()
143
+ for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size {
144
+ block.Encrypt(decrypted[bs:be], data[bs:be])
145
+ }
146
+ return decrypted
147
+ }
148
+
149
+ func PKCS7Padding(ciphertext []byte, blockSize int) []byte {
150
+ padding := blockSize - len(ciphertext)%blockSize
151
+ padtext := bytes.Repeat([]byte{byte(padding)}, padding)
152
+ return append(ciphertext, padtext...)
153
+ }
154
+
155
+ func hmacSha1(data string, secret string) string {
156
+ h := hmac.New(sha1.New, []byte(secret))
157
+ h.Write([]byte(data))
158
+ return hex.EncodeToString(h.Sum(nil))
159
+ }
160
+
161
+ func getMd5(data []byte) []byte {
162
+ h := md5.New()
163
+ h.Write(data)
164
+ return h.Sum(nil)
165
+ }
166
+
167
+ func decodeURIComponent(str string) string {
168
+ r, _ := url.PathUnescape(str)
169
+ //r = strings.ReplaceAll(r, " ", "+")
170
+ return r
171
+ }
172
+
173
+ func Random(v string) string {
174
+ reg := regexp.MustCompilePOSIX("[xy]")
175
+ data := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte {
176
+ var i int64
177
+ t := int64(16 * myrand.Rand.Float32())
178
+ if msg[0] == 120 {
179
+ i = t
180
+ } else {
181
+ i = 3&t | 8
182
+ }
183
+ return []byte(strconv.FormatInt(i, 16))
184
+ })
185
+ return string(data)
186
+ }
drivers/189/login.go ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package _189
2
+
3
+ import (
4
+ "errors"
5
+ "strconv"
6
+
7
+ "github.com/alist-org/alist/v3/pkg/utils"
8
+ log "github.com/sirupsen/logrus"
9
+ )
10
+
11
+ type AppConf struct {
12
+ Data struct {
13
+ AccountType string `json:"accountType"`
14
+ AgreementCheck string `json:"agreementCheck"`
15
+ AppKey string `json:"appKey"`
16
+ ClientType int `json:"clientType"`
17
+ IsOauth2 bool `json:"isOauth2"`
18
+ LoginSort string `json:"loginSort"`
19
+ MailSuffix string `json:"mailSuffix"`
20
+ PageKey string `json:"pageKey"`
21
+ ParamId string `json:"paramId"`
22
+ RegReturnUrl string `json:"regReturnUrl"`
23
+ ReqId string `json:"reqId"`
24
+ ReturnUrl string `json:"returnUrl"`
25
+ ShowFeedback string `json:"showFeedback"`
26
+ ShowPwSaveName string `json:"showPwSaveName"`
27
+ ShowQrSaveName string `json:"showQrSaveName"`
28
+ ShowSmsSaveName string `json:"showSmsSaveName"`
29
+ Sso string `json:"sso"`
30
+ } `json:"data"`
31
+ Msg string `json:"msg"`
32
+ Result string `json:"result"`
33
+ }
34
+
35
+ type EncryptConf struct {
36
+ Result int `json:"result"`
37
+ Data struct {
38
+ UpSmsOn string `json:"upSmsOn"`
39
+ Pre string `json:"pre"`
40
+ PreDomain string `json:"preDomain"`
41
+ PubKey string `json:"pubKey"`
42
+ } `json:"data"`
43
+ }
44
+
45
+ func (d *Cloud189) newLogin() error {
46
+ url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action"
47
+ res, err := d.client.R().Get(url)
48
+ if err != nil {
49
+ return err
50
+ }
51
+ // Is logged in
52
+ redirectURL := res.RawResponse.Request.URL
53
+ if redirectURL.String() == "https://cloud.189.cn/web/main" {
54
+ return nil
55
+ }
56
+ lt := redirectURL.Query().Get("lt")
57
+ reqId := redirectURL.Query().Get("reqId")
58
+ appId := redirectURL.Query().Get("appId")
59
+ headers := map[string]string{
60
+ "lt": lt,
61
+ "reqid": reqId,
62
+ "referer": redirectURL.String(),
63
+ "origin": "https://open.e.189.cn",
64
+ }
65
+ // get app Conf
66
+ var appConf AppConf
67
+ res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
68
+ "version": "2.0",
69
+ "appKey": appId,
70
+ }).SetResult(&appConf).Post("https://open.e.189.cn/api/logbox/oauth2/appConf.do")
71
+ if err != nil {
72
+ return err
73
+ }
74
+ log.Debugf("189 AppConf resp body: %s", res.String())
75
+ if appConf.Result != "0" {
76
+ return errors.New(appConf.Msg)
77
+ }
78
+ // get encrypt conf
79
+ var encryptConf EncryptConf
80
+ res, err = d.client.R().SetHeaders(headers).SetFormData(map[string]string{
81
+ "appId": appId,
82
+ }).Post("https://open.e.189.cn/api/logbox/config/encryptConf.do")
83
+ if err != nil {
84
+ return err
85
+ }
86
+ err = utils.Json.Unmarshal(res.Body(), &encryptConf)
87
+ if err != nil {
88
+ return err
89
+ }
90
+ log.Debugf("189 EncryptConf resp body: %s\n%+v", res.String(), encryptConf)
91
+ if encryptConf.Result != 0 {
92
+ return errors.New("get EncryptConf error:" + res.String())
93
+ }
94
+ // TODO: getUUID? needcaptcha
95
+ // login
96
+ loginData := map[string]string{
97
+ "version": "v2.0",
98
+ "apToken": "",
99
+ "appKey": appId,
100
+ "accountType": appConf.Data.AccountType,
101
+ "userName": encryptConf.Data.Pre + RsaEncode([]byte(d.Username), encryptConf.Data.PubKey, true),
102
+ "epd": encryptConf.Data.Pre + RsaEncode([]byte(d.Password), encryptConf.Data.PubKey, true),
103
+ "captchaType": "",
104
+ "validateCode": "",
105
+ "smsValidateCode": "",
106
+ "captchaToken": "",
107
+ "returnUrl": appConf.Data.ReturnUrl,
108
+ "mailSuffix": appConf.Data.MailSuffix,
109
+ "dynamicCheck": "FALSE",
110
+ "clientType": strconv.Itoa(appConf.Data.ClientType),
111
+ "cb_SaveName": "3",
112
+ "isOauth2": strconv.FormatBool(appConf.Data.IsOauth2),
113
+ "state": "",
114
+ "paramId": appConf.Data.ParamId,
115
+ }
116
+ res, err = d.client.R().SetHeaders(headers).SetFormData(loginData).Post("https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do")
117
+ if err != nil {
118
+ return err
119
+ }
120
+ log.Debugf("189 login resp body: %s", res.String())
121
+ loginResult := utils.Json.Get(res.Body(), "result").ToInt()
122
+ if loginResult != 0 {
123
+ return errors.New(utils.Json.Get(res.Body(), "msg").ToString())
124
+ }
125
+ return nil
126
+ }