Compare commits

...

89 Commits

Author SHA1 Message Date
efe0e6af22 feat: silent start, stop and restart 2022-11-11 18:42:06 +08:00
00de9bf16d fix!: sign with the raw path instead of filename (#2258) 2022-11-11 16:24:25 +08:00
1743110a70 fix(123): incorrect order_by (close #2285) 2022-11-10 21:47:13 +08:00
0352a8e028 feat: add alist v2 driver (#2281) 2022-11-10 17:42:12 +08:00
c601bb794b feat(123): support mail login (close #2218 pr #2276) 2022-11-10 09:34:48 +08:00
42865486f1 fix(local): deal with relative symlink dir (#2274) 2022-11-09 18:15:42 +08:00
44f5cf40ef fix(115): update 115 driver lib to fix some bugs (#2275)
* fix duplicate cookies in client.List func
* rm useless cookie when init
2022-11-09 18:15:06 +08:00
c3ab378ac5 feat(google_drive): support shortcut (close #2268) 2022-11-09 16:19:33 +08:00
cdcbfb24c4 fix(local): directory handle (#2262)
* fix(local): check symlink dir

* fix(local): set size of dir to 0 (close #2264)
2022-11-09 11:20:09 +08:00
e05e2fd663 chore: change default timeout (close #2252) 2022-11-08 20:37:42 +08:00
6639cab1ae feat(google_drive): chunk upload (close #2241) 2022-11-07 20:58:52 +08:00
8241f0999a feat(google_drive): override upload (close #1880) 2022-11-07 20:35:35 +08:00
f3a5e3702d fix(189): force use CN time zone (close #2240) 2022-11-07 16:37:47 +08:00
46701a176d feat(aria2): mark aria2 seeding as complete (#2223)
Currently if using aria2 to download a torrent file, it does not
consider seeding + active as completed, so the torrent download task
only completes as aria2 stops seeding.

This commit uses seeder property of TaskInfo, and mark tasks with active
status and true seeder as complete.
2022-11-06 16:20:09 +08:00
26a29f20c3 fix: missed encode path while use down proxy (close #2208) 2022-11-06 14:46:47 +08:00
18cd45d257 fix: disable cache for 302 redirect (close #2216) 2022-11-05 15:54:51 +08:00
f0a533a77a feat(115): put UA as a variable (#2217)
In special cases, developers can pass in custom UA to solve the speed limit problem
Mainly for developers calling from outside
2022-11-05 15:50:57 +08:00
619a9aeb6c feat(115): add qrcode login (#2206) 2022-11-04 21:16:52 +08:00
7bfa5876ed fix(189pc): fix typo 2022-11-01 19:32:40 +08:00
f95ab6ee57 docs: add 115 to readme [skip ci] 2022-11-01 19:28:24 +08:00
e75f19e9c0 feat: add Referrer-Policy while redirect (pr #2160) 2022-11-01 19:19:08 +08:00
1c212f6c30 feat!: force to use the bin dir as the data dir (close #2108)
- move default log path to `data/log/log.log`
- replace `--conf` with `--data`
2022-11-01 19:16:23 +08:00
141419056d feat(115): add cloud 115 driver (#2164)
close #2112
close #1598
close #894
2022-11-01 15:31:31 +08:00
aabfe49cb9 docs: change contributors show [skip ci] 2022-10-30 15:26:31 +08:00
a3b631f9e9 fix(smb): remount smb before each operation (close #2123 pr #2140) 2022-10-30 15:05:07 +08:00
18165eb50d fix(123): get real url (#2135)
123 今天更新多加了一层跳转`https://web-pro.cjjd18.com/download/?params=base64encode(rawurl)`,导致ip如果不符则可能下载返回403,在服务器端处理获取rawurl
2022-10-27 17:02:35 +08:00
061c462f0b feat(mediatrack): get real url (#2132)
* feat:get real url for mediatrack

redirect token 一次有效,点击第二次就抛出了`400`错误。本次提交直接获取302后的真实链接返回前端

* add cache

cache 60 Second
2022-10-27 15:26:08 +08:00
5f79d665d9 feat: add alist v3 driver (close #1833 pr #2129)
* feat: add alist v3 driver (close #1833)

* chore: use generics

Co-authored-by: Noah Hsu <i@nn.ci>
2022-10-27 10:54:49 +08:00
f0cc0a76a9 chore: fix typos (#2122)
* refactor: fix typos

* Update help.go

Co-authored-by: Noah Hsu <i@nn.ci>
2022-10-26 14:05:56 +08:00
dd4674e486 feat: add smb driver (close #1746) (#2114)
* feat: add smb driver (close #1746)

* Update driver.go
2022-10-25 23:00:23 +08:00
0019959eec fix: delete cache if files is empty 2022-10-25 16:42:06 +08:00
3e9c38697d fix: use utils.Log during static file router init (#2100)
formerly the log is not in stderr
2022-10-24 23:33:57 +08:00
e3b7c41199 docs: update demo url & sponsor content [skip ci] 2022-10-24 22:48:36 +08:00
a2c808c8ce fix: incorrect root path of initial storage for dev mode 2022-10-23 16:26:14 +08:00
da7e17aa38 feat(local): add show hidden config (#2087) 2022-10-23 14:53:07 +08:00
02df3759df docs: fix typo [skip ci] 2022-10-20 14:29:28 +08:00
4fef500795 feat(user): set default password of init user from env (#2058)
add init user default password

Signed-off-by: ysicing <i@ysicing.me>

Signed-off-by: ysicing <i@ysicing.me>
2022-10-19 20:06:06 +08:00
07ece452b3 docs: fix docker link [skip ci] 2022-10-19 17:08:01 +08:00
b8cf02ca68 fix(aria2): retry 5 times for get status (close #1857) 2022-10-18 15:27:19 +08:00
3db798a82a feat(google_photo): Add categories in root, add album support. (#2046)
* feat(google_photo): Add categories in root, add album support.

* fix(google_photo): Remove else block in `drive/google_photo/types.go:60`
2022-10-18 15:19:05 +08:00
45cc0cedbd fix(s3): mkdir and delete (close #2029) 2022-10-18 15:10:47 +08:00
2efade123e fix(189pc):slice bounds out of range close #2045 2022-10-17 22:39:51 +08:00
fc393f743f fix(thunder):no additional processing when the deviceID is correct 2022-10-17 22:37:17 +08:00
0e99e7e9b9 fix(thunder,189pc): some known problems 2022-10-17 00:54:39 +08:00
7a95850c1b fix(google_drive): incorrect ModifiedTime (close #2002) 2022-10-14 14:17:33 +08:00
549355bb29 build: change golang version 2022-10-12 17:35:44 +08:00
55aa8ee3b1 fix: version print of build script [skip ci] 2022-10-12 17:24:04 +08:00
1c22fc367e docs: change badges in readme 2022-10-12 17:08:40 +08:00
5ea8d62aa4 fix(onedrive): unable to operate if path contains % (close #1965) 2022-10-11 14:21:58 +08:00
baebc2fbe9 fix: can't delete disabled storage (close #1942) 2022-10-09 22:20:48 +08:00
8c69260972 fix(webdav): set mime by ext if it's empty 2022-10-09 19:29:55 +08:00
30f992c6a8 feat(onedrive): customize chunk size (close #1927) 2022-10-08 22:23:33 +08:00
dcaaae366b feat: add support for mega.nz (close 1553) 2022-10-08 22:16:41 +08:00
284035823f feat: add Google Photo support (#1853)
* feat: add Google Photo support

* fix: fetch all pages

* chore(google_photo): add meta info

Co-authored-by: Noah Hsu <i@nn.ci>
2022-10-07 20:36:56 +08:00
be8ff92414 docs: replace qq with discord [skip ci] 2022-10-05 14:17:00 +08:00
a4c846a424 chore(onedrive): set default value for region 2022-10-01 20:09:57 +08:00
451e418b18 perf: return cache before check obj to reduce recursion 2022-09-28 21:19:36 +08:00
4e13b1a83c perf: modify onedrive upload chunk size (#1831 close #1790)
improve onedrive upload speed
2022-09-27 20:29:54 +08:00
9d2e9887af docs: create FUNDING.yml [skip ci] 2022-09-27 14:41:43 +08:00
dc73c2e97d fix: custom token expires in doesn't work 2022-09-27 14:23:56 +08:00
a624121095 ci: manual trigger github actions 2022-09-27 14:12:36 +08:00
9d9c79179b feat: custom token expires in 2022-09-27 14:05:00 +08:00
b7479651e1 fix: incorrect base_path from site_url (close #1830) 2022-09-27 13:56:32 +08:00
2fc0ccbfe0 fix: don't init aria2 in new goroutine (close #1752) 2022-09-26 15:11:08 +08:00
f86ad1dce4 fix: create temp dir perm with 777 (close #1813) 2022-09-26 14:48:59 +08:00
f0181d92cd fix: keep type of setting item is correct 2022-09-25 21:20:32 +08:00
5ac6a30c56 fix: use get_share_link_download_url if can't get_download_url (close #1753) 2022-09-25 20:32:11 +08:00
96d8a382e8 fix(aliyundrive_share): reget share token if token expired (close #1798) 2022-09-25 20:14:33 +08:00
7c32af4649 refactor!: move api_url and base_path to config file 2022-09-25 17:57:54 +08:00
03dbb3a403 chore: fix typo of env name 2022-09-25 17:41:04 +08:00
a570e4c7a0 fix: some settings don't take effect at startup 2022-09-23 20:37:49 +08:00
539c47bd3b chore: change log if aria2 not ready 2022-09-23 20:04:47 +08:00
b6d9018ebd fix: sorting by modified doesn't work (close #1756) 2022-09-23 12:30:32 +08:00
c929888e39 fix(123): change remove api (close #1760) 2022-09-23 12:28:57 +08:00
af946ff13e fix(baidu_photo): cannot download when proxy is opened 2022-09-23 01:15:12 +08:00
0039dc18e1 fix: set cdn to basePath if cdn is empty 2022-09-22 17:11:45 +08:00
4d6ab53336 feat: add form upload api (close #1693 #1709) 2022-09-22 16:53:58 +08:00
c7f6684eed chore: add provider to fs list resp 2022-09-22 16:04:10 +08:00
b71ecc8e89 chore: add a default polyfill to head 2022-09-22 11:29:39 +08:00
3537153b91 feat: add aliyundrive share driver (close #1215) 2022-09-21 22:00:06 +08:00
9382f66f87 fix(aliyundrive): thumbnail missed 2022-09-21 21:59:07 +08:00
656f5f112c fix(ftp): nil pointer dereference (close #1722) 2022-09-20 22:23:22 +08:00
9181861f47 fix: illegal files are not displayed (close #1729) 2022-09-20 20:14:38 +08:00
1ab73e0742 feat: add lanzou driver 2022-09-20 15:29:40 +08:00
57686d9df1 fix(189): file size missed 2022-09-19 19:35:07 +08:00
ca177cc3b9 fix: set default mimetype to empty string (close #1710) 2022-09-19 18:58:40 +08:00
d8dc8d8623 fix: dir duplicate creation (close #1687) 2022-09-19 13:43:23 +08:00
5548ab62ac fix: write does not take effect on the current dir (close #1711) 2022-09-19 13:35:37 +08:00
d6d82c3138 fix: page crashes if ipa name contains chinese (close #1712) 2022-09-19 13:33:23 +08:00
117 changed files with 4066 additions and 516 deletions

View File

@ -1,98 +0,0 @@
{
"files": [
"CONTRIBUTORS.md"
],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "Xhofe",
"name": "Xhofe",
"avatar_url": "https://avatars.githubusercontent.com/u/36558727?v=4",
"profile": "http://nn.ci",
"contributions": [
"code",
"ideas",
"doc"
]
},
{
"login": "foxxorcat",
"name": "foxxorcat",
"avatar_url": "https://avatars.githubusercontent.com/u/95907542?v=4",
"profile": "https://github.com/foxxorcat",
"contributions": [
"code"
]
},
{
"login": "DaoChen6",
"name": "道辰",
"avatar_url": "https://avatars.githubusercontent.com/u/63903027?v=4",
"profile": "https://www.iflu.cf/",
"contributions": [
"doc"
]
},
{
"login": "vg-land",
"name": "vg-land",
"avatar_url": "https://avatars.githubusercontent.com/u/16739728?v=4",
"profile": "https://vg-land.github.io/",
"contributions": [
"code"
]
},
{
"login": "Clansty",
"name": "凌莞~(=^▽^=)",
"avatar_url": "https://avatars.githubusercontent.com/u/18461360?v=4",
"profile": "https://c5y.moe",
"contributions": [
"doc"
]
},
{
"login": "Windman1320",
"name": "Windman",
"avatar_url": "https://avatars.githubusercontent.com/u/9999486?v=4",
"profile": "https://github.com/Windman1320",
"contributions": [
"code"
]
},
{
"login": "ericarena",
"name": "ericarena",
"avatar_url": "https://avatars.githubusercontent.com/u/4518927?v=4",
"profile": "https://github.com/ericarena",
"contributions": [
"code"
]
},
{
"login": "WntFlm",
"name": "WntFlm",
"avatar_url": "https://avatars.githubusercontent.com/u/34620278?v=4",
"profile": "https://github.com/WntFlm",
"contributions": [
"code"
]
},
{
"login": "XZB-1248",
"name": "XZB-1248",
"avatar_url": "https://avatars.githubusercontent.com/u/28593573?v=4",
"profile": "https://github.com/XZB-1248",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"projectName": "alist",
"projectOwner": "alist-org",
"repoType": "github",
"repoHost": "https://github.com",
"skipCi": true
}

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

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

View File

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

@ -25,5 +25,6 @@ bin/*
data/
log/
lang/
daemon/
public/dist/*
!public/dist/README.md

View File

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

View File

@ -1,33 +0,0 @@
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-9-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="http://nn.ci"><img src="https://avatars.githubusercontent.com/u/36558727?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Xhofe</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Xhofe" title="Code">💻</a> <a href="#ideas-Xhofe" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/alist-org/alist/commits?author=Xhofe" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/foxxorcat"><img src="https://avatars.githubusercontent.com/u/95907542?v=4?s=100" width="100px;" alt=""/><br /><sub><b>foxxorcat</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=foxxorcat" title="Code">💻</a></td>
<td align="center"><a href="https://www.iflu.cf/"><img src="https://avatars.githubusercontent.com/u/63903027?v=4?s=100" width="100px;" alt=""/><br /><sub><b>道辰</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=DaoChen6" title="Documentation">📖</a></td>
<td align="center"><a href="https://vg-land.github.io/"><img src="https://avatars.githubusercontent.com/u/16739728?v=4?s=100" width="100px;" alt=""/><br /><sub><b>vg-land</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=vg-land" title="Code">💻</a></td>
<td align="center"><a href="https://c5y.moe"><img src="https://avatars.githubusercontent.com/u/18461360?v=4?s=100" width="100px;" alt=""/><br /><sub><b>凌莞~(=^▽^=)</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Clansty" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Windman1320"><img src="https://avatars.githubusercontent.com/u/9999486?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Windman</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=Windman1320" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ericarena"><img src="https://avatars.githubusercontent.com/u/4518927?v=4?s=100" width="100px;" alt=""/><br /><sub><b>ericarena</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=ericarena" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/WntFlm"><img src="https://avatars.githubusercontent.com/u/34620278?v=4?s=100" width="100px;" alt=""/><br /><sub><b>WntFlm</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=WntFlm" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/XZB-1248"><img src="https://avatars.githubusercontent.com/u/28593573?v=4?s=100" width="100px;" alt=""/><br /><sub><b>XZB-1248</b></sub></a><br /><a href="https://github.com/alist-org/alist/commits?author=XZB-1248" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@ -1,35 +1,45 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂A file list program that supports multiple storage, powered by Gin and Solidjs.</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
English | [中文](./README_cn.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -52,6 +62,13 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
- [x] [BaiduNetdisk](http://pan.baidu.com/)
- [x] [Quark](https://pan.quark.cn)
- [x] [Thunder](https://pan.xunlei.com)
- [x] [Lanzou](https://www.lanzou.com/)
- [x] [Aliyundrive share](https://www.aliyundrive.com/)
- [x] [Google photo](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [Baidu photo](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [x] Easy to deploy and out-of-the-box
- [x] File preview (PDF, markdown, code, plain text, ...)
- [x] Image preview in gallery mode
@ -76,17 +93,29 @@ English | [中文](./README_cn.md) | [Contributors](./CONTRIBUTORS.md) | [Contri
## Demo
<https://pan.nn.ci>
<https://al.nn.ci>
## Discussion
Please go to our [discussion forum](https://github.com/Xhofe/alist/discussions) for general questions, **issues are for bug reports and feature request only.**
## Special sponsors
## Sponsor
AList is an open-source software, if you happen to like this project and want me to keep going, please consider sponsoring me or providing a single donation! Thanks for all the love and support:
https://alist.nn.ci/guide/sponsor.html
### Special sponsors
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠!](https://kinhdown.com/?Type=Tutorials)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠Q群 -> 786799372](https://kinhdown.com)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## Contributors
Thanks goes to these wonderful people:
[![Contributors](http://contributors.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)
## License
The `AList` is open-source software licensed under the AGPL-3.0 license.
@ -100,4 +129,4 @@ The `AList` is open-source software licensed under the AGPL-3.0 license.
---
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@QQGroup](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@Blog](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@TelegramGroup](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -1,35 +1,45 @@
<div align="center">
<a href="https://alist.nn.ci"><img height="100px" alt="logo" src="https://cdn.jsdelivr.net/gh/alist-org/logo@main/logo.svg"/></a>
<p><em>🗂一个支持多存储的文件列表程序,使用 Gin 和 Solidjs。</em></p>
<div>
<a href="https://goreportcard.com/report/github.com/alist-org/alist/v3">
<img src="https://goreportcard.com/badge/github.com/alist-org/alist/v3" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/blob/main/LICENSE">
<img src="https://img.shields.io/github/license/Xhofe/alist" alt="License" />
</a>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/actions?query=workflow%3ABuild">
<img src="https://img.shields.io/github/workflow/status/Xhofe/alist/build" alt="Build status" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/release/Xhofe/alist" alt="latest version" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA" alt="Downloads" />
</a>
<a title="Crowdin" target="_blank" href="https://crwd.in/alist">
<img src="https://badges.crowdin.net/alist/localized.svg">
</a>
<a href="https://pay.xhofe.top">
<img src="https://img.shields.io/badge/%24-sponsor-ff69b4.svg" alt="sponsor" />
</div>
<div>
<a href="https://github.com/Xhofe/alist/discussions">
<img src="https://img.shields.io/github/discussions/Xhofe/alist?color=%23ED8936" alt="discussions" />
</a>
<a href="https://discord.gg/F4ymsH4xv2">
<img src="https://img.shields.io/discord/1018870125102895134?logo=discord" alt="discussions" />
</a>
<a href="https://github.com/Xhofe/alist/releases">
<img src="https://img.shields.io/github/downloads/Xhofe/alist/total?color=%239F7AEA&logo=github" alt="Downloads" />
</a>
<a href="https://hub.docker.com/r/xhofe/alist">
<img src="https://img.shields.io/docker/pulls/xhofe/alist?color=%2348BB78&logo=docker&label=pulls" alt="Downloads" />
</a>
<a href="https://alist.nn.ci/zh/guide/sponsor.html">
<img src="https://img.shields.io/badge/%24-sponsor-F87171.svg" alt="sponsor" />
</a>
</div>
</div>
---
[English](./README.md) | 中文 | [Contributors](./CONTRIBUTORS.md) | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
[English](./README.md) | 中文 | [Contributing](./CONTRIBUTING.md) | [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md)
## Features
@ -52,6 +62,13 @@
- [x] [百度网盘](http://pan.baidu.com/)
- [x] [夸克网盘](https://pan.quark.cn)
- [x] [迅雷网盘](https://pan.xunlei.com)
- [x] [蓝奏云](https://www.lanzou.com/)
- [x] [阿里云盘分享](https://www.aliyundrive.com/)
- [x] [谷歌相册](https://photos.google.com/)
- [x] [Mega.nz](https://mega.nz)
- [x] [一刻相册](https://photo.baidu.com/)
- [x] SMB
- [x] [115](https://115.com/)
- [x] 部署方便,开箱即用
- [x] 文件预览PDF、markdown、代码、纯文本……
- [x] 画廊模式下的图像预览
@ -76,18 +93,29 @@
## Demo
<https://pan.nn.ci>
<https://al.nn.ci>
## Discussion
一般问题请到[讨论论坛](https://github.com/Xhofe/alist/discussions) **issue仅针对错误报告和功能请求。**
## Special sponsors
## Sponsor
AList 是一个开源软件如果你碰巧喜欢这个项目并希望我继续下去请考虑赞助我或提供一个单一的捐款感谢所有的爱和支持https://alist.nn.ci/zh/guide/sponsor.html
### Special sponsors
- [找资源 - 阿里云盘资源搜索引擎](https://zhaoziyuan.la/)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠!](https://kinhdown.com/?Type=Tutorials)
- [KinhDown 百度云盘不限速下载永久免费已稳定运行3年非常可靠Q群 -> 786799372](https://kinhdown.com)
- [JetBrains: Essential tools for software developers and teams](https://www.jetbrains.com/)
## 许可
## Contributors
Thanks goes to these wonderful people:
[![Contributors](http://contributors.nn.ci/api?repo=alist-org/alist&repo=alist-org/alist-web&repo=alist-org/docs)](https://github.com/alist-org/alist/graphs/contributors)
## 许可
`AList` 是在 AGPL-3.0 许可下许可的开源软件。
@ -100,4 +128,4 @@
---
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@QQ群](https://jq.qq.com/?_wv=1027&k=YJJj2Gwb)
> [@博客](https://nn.ci/) · [@GitHub](https://github.com/Xhofe) · [@Telegram群](https://t.me/alist_chat) · [@Discord](https://discord.gg/F4ymsH4xv2)

View File

@ -12,7 +12,8 @@ else
webVersion=$(wget -qO- -t1 -T2 "https://api.github.com/repos/alist-org/alist-web/releases/latest" | grep "tag_name" | head -n 1 | awk -F ":" '{print $2}' | sed 's/\"//g;s/,//g;s/ //g')
fi
echo "build version: $gitTag"
echo "backend version: $version"
echo "frontend version: $webVersion"
ldflags="\
-w -s \

View File

@ -1,8 +1,14 @@
package cmd
import (
"os"
"path/filepath"
"strconv"
"github.com/alist-org/alist/v3/internal/bootstrap"
"github.com/alist-org/alist/v3/internal/bootstrap/data"
"github.com/alist-org/alist/v3/pkg/utils"
log "github.com/sirupsen/logrus"
)
func Init() {
@ -11,3 +17,27 @@ func Init() {
bootstrap.InitDB()
data.InitData()
}
var pid = -1
var pidFile string
func initDaemon() {
ex, err := os.Executable()
if err != nil {
log.Fatal(err)
}
exPath := filepath.Dir(ex)
_ = os.MkdirAll(filepath.Join(exPath, "daemon"), 0700)
pidFile = filepath.Join(exPath, "daemon/pid")
if utils.Exists(pidFile) {
bytes, err := os.ReadFile(pidFile)
if err != nil {
log.Fatal("failed to read pid file", err)
}
id, err := strconv.Atoi(string(bytes))
if err != nil {
log.Fatal("failed to parse pid data", err)
}
pid = id
}
}

View File

@ -1,8 +1,9 @@
package flags
var (
Config string // config file
Debug bool
NoPrefix bool
Dev bool
DataDir string
Debug bool
NoPrefix bool
Dev bool
ForceBinDir bool
)

32
cmd/restart.go Normal file
View File

@ -0,0 +1,32 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"github.com/spf13/cobra"
)
// restartCmd represents the restart command
var restartCmd = &cobra.Command{
Use: "restart",
Short: "Restart alist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
stop()
start()
},
}
func init() {
rootCmd.AddCommand(restartCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// restartCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// restartCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

View File

@ -24,8 +24,9 @@ func Execute() {
}
func init() {
rootCmd.PersistentFlags().StringVar(&flags.Config, "conf", "data/config.json", "config file")
rootCmd.PersistentFlags().StringVar(&flags.DataDir, "data", "data", "config file")
rootCmd.PersistentFlags().BoolVar(&flags.Debug, "debug", false, "start with debug mode")
rootCmd.PersistentFlags().BoolVar(&flags.NoPrefix, "no-prefix", false, "disable env prefix")
rootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", false, "start with dev mode")
rootCmd.PersistentFlags().BoolVar(&flags.ForceBinDir, "force-bin-dir", false, "Force to use the directory where the binary file is located as data directory")
}

View File

@ -61,7 +61,7 @@ the address is defined in config file`,
<-quit
utils.Log.Println("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
utils.Log.Fatal("Server Shutdown:", err)
@ -69,7 +69,7 @@ the address is defined in config file`,
// catching ctx.Done(). timeout of 3 seconds.
select {
case <-ctx.Done():
utils.Log.Println("timeout of 3 seconds.")
utils.Log.Println("timeout of 1 seconds.")
}
utils.Log.Println("Server exiting")
},

71
cmd/start.go Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"os/exec"
"path/filepath"
"strconv"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// startCmd represents the start command
var startCmd = &cobra.Command{
Use: "start",
Short: "Silent start alist server with `--force-bin-dir`",
Run: func(cmd *cobra.Command, args []string) {
start()
},
}
func start() {
initDaemon()
if pid != -1 {
_, err := os.FindProcess(pid)
if err == nil {
log.Info("alist already started, pid ", pid)
return
}
}
args := os.Args
args[1] = "server"
args = append(args, "--force-bin-dir")
cmd := &exec.Cmd{
Path: args[0],
Args: args,
Env: os.Environ(),
}
stdout, err := os.OpenFile(filepath.Join(filepath.Dir(pidFile), "start.log"), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
if err != nil {
log.Fatal(os.Getpid(), ": failed to open start log file:", err)
}
cmd.Stderr = stdout
cmd.Stdout = stdout
err = cmd.Start()
if err != nil {
log.Fatal("failed to start children process: ", err)
}
log.Infof("success start pid: %d", cmd.Process.Pid)
err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0666)
if err != nil {
log.Warn("failed to record pid, you may not be able to stop the program with `./alist stop`")
}
}
func init() {
rootCmd.AddCommand(startCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// startCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

58
cmd/stop.go Normal file
View File

@ -0,0 +1,58 @@
/*
Copyright © 2022 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
// stopCmd represents the stop command
var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop alist server by daemon/pid file",
Run: func(cmd *cobra.Command, args []string) {
stop()
},
}
func stop() {
initDaemon()
if pid == -1 {
log.Info("Seems not have been started. Try use `alist start` to start server.")
return
}
process, err := os.FindProcess(pid)
if err != nil {
log.Errorf("failed to find process by pid: %d, reason: %v", pid, process)
return
}
err = process.Kill()
if err != nil {
log.Errorf("failed to kill process %d: %v", pid, err)
} else {
log.Info("killed process: ", pid)
}
err = os.Remove(pidFile)
if err != nil {
log.Errorf("failed to remove pid file")
}
pid = -1
}
func init() {
rootCmd.AddCommand(stopCmd)
// Here you will define your flags and configuration settings.
// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// stopCmd.PersistentFlags().String("foo", "", "A help for foo")
// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}

98
drivers/115/driver.go Normal file
View File

@ -0,0 +1,98 @@
package _115
import (
"context"
"os"
driver115 "github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
)
type Pan115 struct {
model.Storage
Addition
client *driver115.Pan115Client
}
func (d *Pan115) Config() driver.Config {
return config
}
func (d *Pan115) GetAddition() driver.Additional {
return d.Addition
}
func (d *Pan115) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.login()
}
func (d *Pan115) Drop(ctx context.Context) error {
return nil
}
func (d *Pan115) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(dir.GetID())
if err != nil && !errors.Is(err, driver115.ErrNotExist) {
return nil, err
}
return utils.SliceConvert(files, func(src driver115.File) (model.Obj, error) {
return src, nil
})
}
func (d *Pan115) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
downloadInfo, err := d.client.Download(file.(driver115.File).PickCode)
if err != nil {
return nil, err
}
link := &model.Link{
URL: downloadInfo.Url.Url,
Header: downloadInfo.Header,
}
return link, nil
}
func (d *Pan115) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if _, err := d.client.Mkdir(parentDir.GetID(), dirName); err != nil {
return err
}
return nil
}
func (d *Pan115) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.client.Move(dstDir.GetID(), srcObj.GetID())
}
func (d *Pan115) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return d.client.Rename(srcObj.GetID(), newName)
}
func (d *Pan115) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return d.client.Copy(dstDir.GetID(), srcObj.GetID())
}
func (d *Pan115) Remove(ctx context.Context, obj model.Obj) error {
return d.client.Delete(obj.GetID())
}
func (d *Pan115) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
tempFile, err := utils.CreateTempFile(stream.GetReadCloser())
if err != nil {
return err
}
defer func() {
_ = tempFile.Close()
_ = os.Remove(tempFile.Name())
}()
return d.client.UploadFastOrByMultipart(dstDir.GetID(), stream.GetName(), stream.GetSize(), tempFile)
}
var _ driver.Driver = (*Pan115)(nil)

27
drivers/115/meta.go Normal file
View File

@ -0,0 +1,27 @@
package _115
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
Cookie string `json:"cookie"`
QRCodeToken string `json:"qrcode_token"`
driver.RootID
}
var config = driver.Config{
Name: "115 Cloud",
DefaultRoot: "0",
OnlyProxy: true,
OnlyLocal: true,
}
func New() driver.Driver {
return &Pan115{}
}
func init() {
op.RegisterDriver(config, New)
}

8
drivers/115/types.go Normal file
View File

@ -0,0 +1,8 @@
package _115
import (
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/alist-org/alist/v3/internal/model"
)
var _ model.Obj = (*driver.File)(nil)

49
drivers/115/util.go Normal file
View File

@ -0,0 +1,49 @@
package _115
import (
"fmt"
"github.com/SheltonZhu/115driver/pkg/driver"
"github.com/pkg/errors"
)
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 115Browser/23.9.3.2 115disk/30.1.0"
func (d *Pan115) login() error {
var err error
opts := []driver.Option{
driver.UA(UserAgent),
}
d.client = driver.New(opts...)
cr := &driver.Credential{}
if d.Addition.QRCodeToken != "" {
s := &driver.QRCodeSession{
UID: d.Addition.QRCodeToken,
}
if cr, err = d.client.QRCodeLogin(s); err != nil {
return errors.Wrap(err, "failed to login by qrcode")
}
d.Addition.Cookie = fmt.Sprintf("UID=%s;CID=%s;SEID=%s", cr.UID, cr.CID, cr.SEID)
d.Addition.QRCodeToken = ""
} else if d.Addition.Cookie != "" {
if err = cr.FromCookie(d.Addition.Cookie); err != nil {
return errors.Wrap(err, "failed to login by cookies")
}
d.client.ImportCredential(cr)
} else {
return errors.New("missing cookie or qrcode account")
}
return d.client.LoginCheck()
}
func (d *Pan115) getFiles(fileId string) ([]driver.File, error) {
res := make([]driver.File, 0)
files, err := d.client.List(fileId)
if err != nil {
return nil, err
}
for _, file := range *files {
res = append(res, file)
}
return res, nil
}

View File

@ -6,6 +6,7 @@ import (
"crypto/md5"
"encoding/binary"
"encoding/hex"
"encoding/base64"
"fmt"
"io"
"net/http"
@ -96,6 +97,14 @@ func (d *Pan123) Link(ctx context.Context, file model.Obj, args model.LinkArgs)
if err != nil {
return nil, err
}
nu := u.Query().Get("params")
if nu != "" {
du, _ := base64.StdEncoding.DecodeString(nu)
u, err = url.Parse(string(du))
if err != nil {
return nil, err
}
}
u_ := fmt.Sprintf("https://%s%s", u.Host, u.Path)
res, err := base.NoRedirectClient.R().SetQueryParamsFromValues(u.Query()).Head(u_)
if err != nil {
@ -164,7 +173,7 @@ func (d *Pan123) Remove(ctx context.Context, obj model.Obj) error {
"operation": true,
"fileTrashInfoList": []File{f},
}
_, err := d.request("https://www.123pan.com/a/api/file/trash", http.MethodPost, func(req *resty.Request) {
_, err := d.request("https://www.123pan.com/b/api/file/trash", http.MethodPost, func(req *resty.Request) {
req.SetBody(data)
}, nil)
return err

View File

@ -8,7 +8,7 @@ import (
type Addition struct {
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
OrderBy string `json:"order_by" type:"select" options:"name,fileId,updateAt,createAt" default:"name"`
OrderBy string `json:"order_by" type:"select" options:"file_name,size,update_at" default:"file_name"`
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
driver.RootID
// define other

View File

@ -6,6 +6,7 @@ import (
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
)
@ -13,14 +14,24 @@ import (
// do others that not defined in Driver interface
func (d *Pan123) login() error {
var body base.Json
url := "https://www.123pan.com/api/user/sign_in"
if utils.IsEmailFormat(d.Username) {
body = base.Json{
"mail": d.Username,
"password": d.Password,
"type": 2,
}
} else {
body = base.Json{
"passport": d.Username,
"password": d.Password,
}
}
var resp TokenResp
_, err := base.RestyClient.R().
SetResult(&resp).
SetBody(base.Json{
"passport": d.Username,
"password": d.Password,
}).Post(url)
SetBody(body).Post(url)
if err != nil {
return err
}

View File

@ -178,7 +178,6 @@ func (d *Cloud189) request(url string, method string, callback base.ReqCallback,
func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
res := make([]model.Obj, 0)
pageNum := 1
loc, _ := time.LoadLocation("Local")
for {
var resp Files
_, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) {
@ -200,7 +199,7 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
break
}
for _, folder := range resp.FileListAO.FolderList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", folder.LastOpTime, loc)
lastOpTime := utils.MustParseCNTime(folder.LastOpTime)
res = append(res, &model.Object{
ID: strconv.FormatInt(folder.Id, 10),
Name: folder.Name,
@ -209,12 +208,13 @@ func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) {
})
}
for _, file := range resp.FileListAO.FileList {
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc)
lastOpTime := utils.MustParseCNTime(file.LastOpTime)
res = append(res, &model.ObjThumb{
Object: model.Object{
ID: strconv.FormatInt(file.Id, 10),
Name: file.Name,
Modified: lastOpTime,
Size: file.Size,
},
Thumbnail: model.Thumbnail{Thumbnail: file.Icon.SmallUrl},
})

View File

@ -110,7 +110,9 @@ func ParseHttpHeader(str string) map[string]string {
header := make(map[string]string)
for _, value := range strings.Split(str, "&") {
i := strings.Index(value, "=")
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
if i > 0 {
header[strings.TrimSpace(value[0:i])] = strings.TrimSpace(value[i+1:])
}
}
return header
}

View File

@ -15,6 +15,7 @@ type Addition struct {
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
FamilyID string `json:"family_id"`
RapidUpload bool `json:"rapid_upload"`
NoUseOcr bool `json:"no_use_ocr"`
}
var config = driver.Config{

View File

@ -186,20 +186,19 @@ func (y *Yun189PC) getFiles(ctx context.Context, fileId string) ([]model.Obj, er
func (y *Yun189PC) login() (err error) {
// 初始化登陆所需参数
if y.loginParam == nil {
if y.loginParam == nil || !y.NoUseOcr {
if err = y.initLoginParam(); err != nil {
// 验证码也通过错误返回
return err
}
}
defer func() {
// 销毁验证码
y.VCode = ""
// 销毁登陆参数
y.loginParam = nil
// 遇到错误,重新加载登陆参数
if err != nil {
if err != nil && y.NoUseOcr {
if err1 := y.initLoginParam(); err1 != nil {
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
}
@ -303,44 +302,34 @@ func (y *Yun189PC) initLoginParam() error {
param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
// 判断是否需要验证码
res, err = y.client.R().
SetFormData(map[string]string{
"appKey": APP_ID,
"accountType": ACCOUNT_TYPE,
"userName": param.RsaUsername,
}).
Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
if err != nil {
return err
}
y.loginParam = &param
if res.String() != "0" {
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
"token": param.CaptchaToken,
"REQID": param.ReqId,
"rnd": fmt.Sprint(timestamp()),
}).
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
if err != nil {
return fmt.Errorf("failed to obtain verification code")
imgRes, err := y.client.R().
SetQueryParams(map[string]string{
"token": param.CaptchaToken,
"REQID": param.ReqId,
"rnd": fmt.Sprint(timestamp()),
}).
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
if err != nil {
return fmt.Errorf("failed to obtain verification code")
}
if imgRes.Size() > 20 {
if setting.GetStr(conf.OcrApi) != "" && !y.NoUseOcr {
vRes, err := base.RestyClient.R().
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(setting.GetStr(conf.OcrApi))
if err != nil {
return err
}
if jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
return nil
}
}
// 尝试使用ocr
vRes, err := base.RestyClient.R().
SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())).
Post(setting.GetStr(conf.OcrApi))
if err == nil && jsoniter.Get(vRes.Body(), "status").ToInt() == 200 {
y.VCode = jsoniter.Get(vRes.Body(), "result").ToString()
}
// ocr无法处理返回验证码图片给前端
if len(y.VCode) != 4 {
return fmt.Errorf("need validate code: data:image/png;base64,%s", base64.StdEncoding.EncodeToString(res.Body()))
}
// 返回验证码图片给前端
return fmt.Errorf(`need img validate code: <img src="data:image/png;base64,%s"/>`, base64.StdEncoding.EncodeToString(imgRes.Body()))
}
return nil
}
@ -396,6 +385,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
const DEFAULT int64 = 10485760
var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT)))
requestID := uuid.NewString()
params := Params{
"parentFolderId": dstDir.GetID(),
"fileName": url.QueryEscape(file.GetName()),
@ -417,6 +407,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
var initMultiUpload InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &initMultiUpload)
if err != nil {
return err
@ -451,6 +442,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"partInfo": fmt.Sprintf("%d-%s", i, silceMd5Base64),
"uploadFileId": initMultiUpload.Data.UploadFileID,
@ -486,6 +478,7 @@ func (y *Yun189PC) CommonUpload(ctx context.Context, dstDir model.Obj, file mode
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": initMultiUpload.Data.UploadFileID,
"fileMd5": fileMd5Hex,
@ -542,6 +535,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n")))
}
requestID := uuid.NewString()
// 检测是否支持快传
params := Params{
"parentFolderId": dstDir.GetID(),
@ -564,6 +558,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
var uploadInfo InitMultiUploadResp
_, err = y.request(fullUrl+"/initMultiUpload", http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, params, &uploadInfo)
if err != nil {
return err
@ -575,6 +570,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/getMultiUploadUrls", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"partInfo": strings.Join(silceMd5Base64s, ","),
@ -611,6 +607,7 @@ func (y *Yun189PC) FastUpload(ctx context.Context, dstDir model.Obj, file model.
_, err = y.request(fullUrl+"/commitMultiUploadFile", http.MethodGet,
func(req *resty.Request) {
req.SetContext(ctx)
req.SetHeader("X-Request-ID", requestID)
}, Params{
"uploadFileId": uploadInfo.Data.UploadFileID,
"isLog": "0",

126
drivers/alist_v2/driver.go Normal file
View File

@ -0,0 +1,126 @@
package alist_v2
import (
"context"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
)
type AListV2 struct {
model.Storage
Addition
}
func (d *AListV2) Config() driver.Config {
return config
}
func (d *AListV2) GetAddition() driver.Additional {
return d.Addition
}
func (d *AListV2) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
return err
}
func (d *AListV2) Drop(ctx context.Context) error {
return nil
}
func (d *AListV2) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: dir.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
var files []model.Obj
for _, f := range resp.Data.Files {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: *f.UpdatedAt,
Size: f.Size,
IsFolder: f.Type == 1,
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumbnail},
}
files = append(files, &file)
}
return files, nil
}
//func (d *AList) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AListV2) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/public/path"
var resp common.Resp[PathResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(PathReq{
PageNum: 0,
PageSize: 0,
Path: file.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Data.Files[0].Url,
}, nil
}
func (d *AListV2) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement
}
func (d *AListV2) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotImplement
}
func (d *AListV2) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement
}
func (d *AListV2) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotImplement
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*AListV2)(nil)

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

@ -0,0 +1,26 @@
package alist_v2
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
}
var config = driver.Config{
Name: "AList V2",
LocalSort: true,
NoUpload: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &AListV2{}
})
}

31
drivers/alist_v2/types.go Normal file
View File

@ -0,0 +1,31 @@
package alist_v2
import (
"time"
)
type File struct {
Id string `json:"-"`
Name string `json:"name"`
Size int64 `json:"size"`
Type int `json:"type"`
Driver string `json:"driver"`
UpdatedAt *time.Time `json:"updated_at"`
Thumbnail string `json:"thumbnail"`
Url string `json:"url"`
SizeStr string `json:"size_str"`
TimeStr string `json:"time_str"`
}
type PathResp struct {
Type string `json:"type"`
//Meta Meta `json:"meta"`
Files []File `json:"files"`
}
type PathReq struct {
PageNum int `json:"page_num"`
PageSize int `json:"page_size"`
Password string `json:"password"`
Path string `json:"path"`
}

1
drivers/alist_v2/util.go Normal file
View File

@ -0,0 +1 @@
package alist_v2

128
drivers/alist_v3/driver.go Normal file
View File

@ -0,0 +1,128 @@
package alist_v3
import (
"context"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/alist-org/alist/v3/server/common"
"github.com/alist-org/alist/v3/server/handles"
)
type AListV3 struct {
model.Storage
Addition
}
func (d *AListV3) Config() driver.Config {
return config
}
func (d *AListV3) GetAddition() driver.Additional {
return d.Addition
}
func (d *AListV3) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
// TODO login / refresh token
//op.MustSaveDriverStorage(d)
return err
}
func (d *AListV3) Drop(ctx context.Context) error {
return nil
}
func (d *AListV3) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
url := d.Address + "/api/fs/list"
var resp common.Resp[handles.FsListResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(handles.ListReq{
PageReq: common.PageReq{
Page: 1,
PerPage: 0,
},
Path: dir.GetPath(),
Password: d.Password,
Refresh: false,
}).Post(url)
if err != nil {
return nil, err
}
var files []model.Obj
for _, f := range resp.Data.Content {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name,
Modified: f.Modified,
Size: f.Size,
IsFolder: f.IsDir,
},
Thumbnail: model.Thumbnail{Thumbnail: f.Thumb},
}
files = append(files, &file)
}
return files, nil
}
//func (d *AList) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *AListV3) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
url := d.Address + "/api/fs/get"
var resp common.Resp[handles.FsGetResp]
_, err := base.RestyClient.R().
SetResult(&resp).
SetHeader("Authorization", d.AccessToken).
SetBody(handles.FsGetReq{
Path: file.GetPath(),
Password: d.Password,
}).Post(url)
if err != nil {
return nil, err
}
return &model.Link{
URL: resp.Data.RawURL,
}, nil
}
func (d *AListV3) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
return errs.NotImplement
}
func (d *AListV3) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV3) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
return errs.NotImplement
}
func (d *AListV3) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
return errs.NotImplement
}
func (d *AListV3) Remove(ctx context.Context, obj model.Obj) error {
return errs.NotImplement
}
func (d *AListV3) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
return errs.NotImplement
}
//func (d *AList) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*AListV3)(nil)

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

@ -0,0 +1,26 @@
package alist_v3
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
Address string `json:"url" required:"true"`
Password string `json:"password"`
AccessToken string `json:"access_token"`
}
var config = driver.Config{
Name: "AList V3",
LocalSort: true,
NoUpload: true,
DefaultRoot: "/",
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &AListV3{}
})
}

View File

@ -0,0 +1 @@
package alist_v3

1
drivers/alist_v3/util.go Normal file
View File

@ -0,0 +1 @@
package alist_v3

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,30 @@
package drivers
import (
_ "github.com/alist-org/alist/v3/drivers/115"
_ "github.com/alist-org/alist/v3/drivers/123"
_ "github.com/alist-org/alist/v3/drivers/139"
_ "github.com/alist-org/alist/v3/drivers/189"
_ "github.com/alist-org/alist/v3/drivers/189pc"
_ "github.com/alist-org/alist/v3/drivers/alist_v2"
_ "github.com/alist-org/alist/v3/drivers/alist_v3"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive"
_ "github.com/alist-org/alist/v3/drivers/aliyundrive_share"
_ "github.com/alist-org/alist/v3/drivers/baidu_netdisk"
_ "github.com/alist-org/alist/v3/drivers/baidu_photo"
_ "github.com/alist-org/alist/v3/drivers/ftp"
_ "github.com/alist-org/alist/v3/drivers/google_drive"
_ "github.com/alist-org/alist/v3/drivers/google_photo"
_ "github.com/alist-org/alist/v3/drivers/lanzou"
_ "github.com/alist-org/alist/v3/drivers/local"
_ "github.com/alist-org/alist/v3/drivers/mediatrack"
_ "github.com/alist-org/alist/v3/drivers/mega"
_ "github.com/alist-org/alist/v3/drivers/onedrive"
_ "github.com/alist-org/alist/v3/drivers/pikpak"
_ "github.com/alist-org/alist/v3/drivers/quark"
_ "github.com/alist-org/alist/v3/drivers/s3"
_ "github.com/alist-org/alist/v3/drivers/sftp"
_ "github.com/alist-org/alist/v3/drivers/smb"
_ "github.com/alist-org/alist/v3/drivers/teambition"
_ "github.com/alist-org/alist/v3/drivers/thunder"
_ "github.com/alist-org/alist/v3/drivers/uss"

View File

@ -333,6 +333,7 @@ func (d *BaiduPhoto) linkAlbum(ctx context.Context, file model.Obj, args model.L
URL: res.Header().Get("location"),
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}
@ -369,6 +370,7 @@ func (d *BaiduPhoto) linkFile(ctx context.Context, file model.Obj, args model.Li
URL: downloadUrl.Dlink,
Header: http.Header{
"User-Agent": []string{headers["User-Agent"]},
"Referer": []string{"https://photo.baidu.com/"},
},
//Expiration: &exp,
}

View File

@ -11,7 +11,7 @@ var NoRedirectClient *resty.Client
var RestyClient = NewRestyClient()
var HttpClient = &http.Client{}
var UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
var DefaultTimeout = time.Second * 10
var DefaultTimeout = time.Second * 30
func init() {
NoRedirectClient = resty.New().SetRedirectPolicy(

View File

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

View File

@ -4,11 +4,14 @@ import (
"context"
"fmt"
"net/http"
stdpath "path"
"strconv"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/errs"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/go-resty/resty/v2"
)
@ -33,6 +36,9 @@ func (d *GoogleDrive) Init(ctx context.Context, storage model.Storage) error {
if err != nil {
return err
}
if d.ChunkSize == 0 {
d.ChunkSize = 5
}
return d.refreshToken()
}
@ -112,15 +118,37 @@ func (d *GoogleDrive) Remove(ctx context.Context, obj model.Obj) error {
}
func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
data := base.Json{
"name": stream.GetName(),
"parents": []string{dstDir.GetID()},
obj, _ := op.Get(ctx, d, stdpath.Join(dstDir.GetPath(), stream.GetName()))
var (
e Error
url string
data base.Json
res *resty.Response
err error
)
if obj != nil {
url = fmt.Sprintf("https://www.googleapis.com/upload/drive/v3/files/%s?uploadType=resumable&supportsAllDrives=true", obj.GetID())
data = base.Json{}
} else {
data = base.Json{
"name": stream.GetName(),
"parents": []string{dstDir.GetID()},
}
url = "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
}
req := base.NoRedirectClient.R().
SetHeaders(map[string]string{
"Authorization": "Bearer " + d.AccessToken,
"X-Upload-Content-Type": stream.GetMimetype(),
"X-Upload-Content-Length": strconv.FormatInt(stream.GetSize(), 10),
}).
SetError(&e).SetBody(data)
if obj != nil {
res, err = req.Patch(url)
} else {
res, err = req.Post(url)
}
var e Error
url := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable&supportsAllDrives=true"
res, err := base.NoRedirectClient.R().SetHeader("Authorization", "Bearer "+d.AccessToken).
SetError(&e).SetBody(data).
Post(url)
if err != nil {
return err
}
@ -135,9 +163,13 @@ func (d *GoogleDrive) Put(ctx context.Context, dstDir model.Obj, stream model.Fi
return fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
putUrl := res.Header().Get("location")
_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {
req.SetBody(stream.GetReadCloser())
}, nil)
if stream.GetSize() < d.ChunkSize*1024*1024 {
_, err = d.request(putUrl, http.MethodPut, func(req *resty.Request) {
req.SetHeader("Content-Length", strconv.FormatInt(stream.GetSize(), 10)).SetBody(stream.GetReadCloser())
}, nil)
} else {
err = d.chunkUpload(ctx, stream, putUrl)
}
return err
}

View File

@ -12,6 +12,7 @@ type Addition struct {
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc"`
ClientID string `json:"client_id" required:"true" default:"202264815644.apps.googleusercontent.com"`
ClientSecret string `json:"client_secret" required:"true" default:"X4Z3ca8xfWDb1Voo-F9a7ZxJ"`
ChunkSize int64 `json:"chunk_size" default:"5" help:"chunk size while uploading (unit: MB)"`
}
var config = driver.Config{

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/alist-org/alist/v3/internal/model"
log "github.com/sirupsen/logrus"
)
type TokenError struct {
@ -18,26 +19,36 @@ type Files struct {
}
type File struct {
Id string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
ModifiedTime time.Time `json:"modifiedTime"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"`
Id string `json:"id"`
Name string `json:"name"`
MimeType string `json:"mimeType"`
ModifiedTime time.Time `json:"modifiedTime"`
Size string `json:"size"`
ThumbnailLink string `json:"thumbnailLink"`
ShortcutDetails struct {
TargetId string `json:"targetId"`
TargetMimeType string `json:"targetMimeType"`
} `json:"shortcutDetails"`
}
func fileToObj(f File) *model.ObjThumb {
log.Debugf("google file: %+v", f)
size, _ := strconv.ParseInt(f.Size, 10, 64)
return &model.ObjThumb{
obj := &model.ObjThumb{
Object: model.Object{
ID: f.Id,
Name: f.Name,
Size: size,
Modified: time.Time{},
Modified: f.ModifiedTime,
IsFolder: f.MimeType == "application/vnd.google-apps.folder",
},
Thumbnail: model.Thumbnail{},
}
if f.MimeType == "application/vnd.google-apps.shortcut" {
obj.ID = f.ShortcutDetails.TargetId
obj.IsFolder = f.ShortcutDetails.TargetMimeType == "application/vnd.google-apps.folder"
}
return obj
}
type Error struct {

View File

@ -1,10 +1,14 @@
package google_drive
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/model"
"github.com/go-resty/resty/v2"
log "github.com/sirupsen/logrus"
)
@ -77,7 +81,7 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
}
query := map[string]string{
"orderBy": orderBy,
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink),nextPageToken",
"fields": "files(id,name,mimeType,size,modifiedTime,thumbnailLink,shortcutDetails),nextPageToken",
"pageSize": "1000",
"q": fmt.Sprintf("'%s' in parents and trashed = false", id),
//"includeItemsFromAllDrives": "true",
@ -95,3 +99,25 @@ func (d *GoogleDrive) getFiles(id string) ([]File, error) {
}
return res, nil
}
func (d *GoogleDrive) chunkUpload(ctx context.Context, stream model.FileStreamer, url string) error {
var defaultChunkSize = d.ChunkSize * 1024 * 1024
var finish int64 = 0
for finish < stream.GetSize() {
chunkSize := stream.GetSize() - finish
if chunkSize > defaultChunkSize {
chunkSize = defaultChunkSize
}
_, err := d.request(url, http.MethodPut, func(req *resty.Request) {
req.SetHeaders(map[string]string{
"Content-Length": strconv.FormatInt(chunkSize, 10),
"Content-Range": fmt.Sprintf("bytes %d-%d/%d", finish, finish+chunkSize-1, stream.GetSize()),
}).SetBody(io.LimitReader(stream.GetReadCloser(), chunkSize))
}, nil)
if err != nil {
return err
}
finish += chunkSize
}
return nil
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,186 @@
package google_photo
import (
"fmt"
"net/http"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/go-resty/resty/v2"
)
// do others that not defined in Driver interface
const (
FETCH_ALL = "all"
FETCH_ALBUMS = "albums"
FETCH_ROOT = "root"
FETCH_SHARE_ALBUMS = "share_albums"
)
func (d *GooglePhoto) refreshToken() error {
url := "https://www.googleapis.com/oauth2/v4/token"
var resp base.TokenResp
var e TokenError
_, err := base.RestyClient.R().SetResult(&resp).SetError(&e).
SetFormData(map[string]string{
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
"refresh_token": d.RefreshToken,
"grant_type": "refresh_token",
}).Post(url)
if err != nil {
return err
}
if e.Error != "" {
return fmt.Errorf(e.Error)
}
d.AccessToken = resp.AccessToken
return nil
}
func (d *GooglePhoto) request(url string, method string, callback base.ReqCallback, resp interface{}, headers map[string]string) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeader("Authorization", "Bearer "+d.AccessToken)
req.SetHeader("Accept-Encoding", "gzip")
if headers != nil {
req.SetHeaders(headers)
}
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
var e Error
req.SetError(&e)
res, err := req.Execute(method, url)
if err != nil {
return nil, err
}
if e.Error.Code != 0 {
if e.Error.Code == 401 {
err = d.refreshToken()
if err != nil {
return nil, err
}
return d.request(url, method, callback, resp, headers)
}
return nil, fmt.Errorf("%s: %v", e.Error.Message, e.Error.Errors)
}
return res.Body(), nil
}
func (d *GooglePhoto) getFiles(id string) ([]MediaItem, error) {
switch id {
case FETCH_ALL:
return d.getAllMedias()
case FETCH_ALBUMS:
return d.getAlbums()
case FETCH_SHARE_ALBUMS:
return d.getShareAlbums()
case FETCH_ROOT:
return d.getFakeRoot()
default:
return d.getMedias(id)
}
}
func (d *GooglePhoto) getFakeRoot() ([]MediaItem, error) {
return []MediaItem{
{
Id: FETCH_ALL,
Title: "全部媒体",
},
{
Id: FETCH_ALBUMS,
Title: "全部影集",
},
{
Id: FETCH_SHARE_ALBUMS,
Title: "共享影集",
},
}, nil
}
func (d *GooglePhoto) getAlbums() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/albums",
map[string]string{
"fields": "albums(id,title,coverPhotoBaseUrl),nextPageToken",
"pageSize": "50",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getShareAlbums() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/sharedAlbums",
map[string]string{
"fields": "sharedAlbums(id,title,coverPhotoBaseUrl),nextPageToken",
"pageSize": "50",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getMedias(albumId string) ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/mediaItems:search",
map[string]string{
"fields": "mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",
"pageSize": "100",
"albumId": albumId,
"pageToken": "first",
}, http.MethodPost)
}
func (d *GooglePhoto) getAllMedias() ([]MediaItem, error) {
return d.fetchItems(
"https://photoslibrary.googleapis.com/v1/mediaItems",
map[string]string{
"fields": "mediaItems(id,baseUrl,mimeType,mediaMetadata,filename),nextPageToken",
"pageSize": "100",
"pageToken": "first",
},
http.MethodGet)
}
func (d *GooglePhoto) getMedia(id string) (MediaItem, error) {
var resp MediaItem
query := map[string]string{
"fields": "baseUrl,mimeType",
}
_, err := d.request(fmt.Sprintf("https://photoslibrary.googleapis.com/v1/mediaItems/%s", id), http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return resp, err
}
return resp, nil
}
func (d *GooglePhoto) fetchItems(url string, query map[string]string, method string) ([]MediaItem, error){
res := make([]MediaItem, 0)
for query["pageToken"] != "" {
if query["pageToken"] == "first" {
query["pageToken"] = ""
}
var resp Items
_, err := d.request(url, method, func(req *resty.Request) {
req.SetQueryParams(query)
}, &resp, nil)
if err != nil {
return nil, err
}
query["pageToken"] = resp.NextPageToken
res = append(res, resp.MediaItems...)
res = append(res, resp.Albums...)
res = append(res, resp.SharedAlbums...)
}
return res, nil
}

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

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

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

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

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

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

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

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

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

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

View File

@ -68,7 +68,7 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
}
var files []model.Obj
for _, f := range rawFiles {
if strings.HasPrefix(f.Name(), ".") {
if !d.ShowHidden && strings.HasPrefix(f.Name(), ".") {
continue
}
thumb := ""
@ -77,12 +77,17 @@ func (d *Local) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([
thumb = utils.EncodePath(thumb, true)
thumb += "?type=thumb"
}
isFolder := f.IsDir() || isSymlinkDir(f, fullPath)
size := f.Size()
if isFolder {
size = 0
}
file := model.ObjThumb{
Object: model.Object{
Name: f.Name(),
Modified: f.ModTime(),
Size: f.Size(),
IsFolder: f.IsDir(),
Size: size,
IsFolder: isFolder,
},
Thumbnail: model.Thumbnail{
Thumbnail: thumb,
@ -101,12 +106,17 @@ func (d *Local) Get(ctx context.Context, path string) (model.Obj, error) {
}
return nil, err
}
isFolder := f.IsDir() || isSymlinkDir(f, path)
size := f.Size()
if isFolder {
size = 0
}
file := model.Object{
Path: path,
Name: f.Name(),
Modified: f.ModTime(),
Size: f.Size(),
IsFolder: f.IsDir(),
Size: size,
IsFolder: isFolder,
}
return &file, nil
}

View File

@ -7,7 +7,8 @@ import (
type Addition struct {
driver.RootPath
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
Thumbnail bool `json:"thumbnail" required:"true" help:"enable thumbnail"`
ShowHidden bool `json:"show_hidden" default:"true" required:"false" help:"show hidden directories and files"`
}
var config = driver.Config{

View File

@ -1 +1,25 @@
package local
import (
"io/fs"
"os"
"path/filepath"
)
func isSymlinkDir(f fs.FileInfo, path string) bool {
if f.Mode()&os.ModeSymlink == os.ModeSymlink {
dst, err := os.Readlink(filepath.Join(path, f.Name()))
if err != nil {
return false
}
if !filepath.IsAbs(dst) {
dst = filepath.Join(path, dst)
}
stat, err := os.Stat(dst)
if err != nil {
return false
}
return stat.IsDir()
}
return false
}

View File

@ -10,6 +10,7 @@ import (
"os"
"path"
"strconv"
"time"
"github.com/alist-org/alist/v3/drivers/base"
"github.com/alist-org/alist/v3/internal/driver"
@ -91,7 +92,21 @@ func (d *MediaTrack) Link(ctx context.Context, file model.Obj, args model.LinkAr
}
token := utils.Json.Get(body, "data", "token").ToString()
url = "https://kayn.api.mediatrack.cn/v1/download/redirect?token=" + token
return &model.Link{URL: url}, nil
res, err := base.NoRedirectClient.R().Get(url)
if err != nil {
return nil, err
}
log.Debug(res.String())
link := model.Link{
URL: url,
}
log.Debugln("res code: ", res.StatusCode())
if res.StatusCode() == 302 {
link.URL = res.Header().Get("location")
expired := time.Duration(60) * time.Second
link.Expiration = &expired
}
return &link, nil
}
func (d *MediaTrack) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {

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

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

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

@ -0,0 +1,26 @@
package mega
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
// Usually one of two
//driver.RootPath
//driver.RootID
Email string `json:"email" required:"true"`
Password string `json:"password" required:"true"`
}
var config = driver.Config{
Name: "Mega_nz",
LocalSort: true,
OnlyLocal: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &Mega{}
})
}

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

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

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

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

View File

@ -34,6 +34,9 @@ func (d *Onedrive) Init(ctx context.Context, storage model.Storage) error {
if err != nil {
return err
}
if d.ChunkSize < 1 {
d.ChunkSize = 5
}
return d.refreshToken()
}

View File

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

View File

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

View File

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

View File

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

View File

@ -52,11 +52,12 @@ func getKey(path string, dir bool) string {
return path
}
// var defaultPlaceholderName = ".placeholder"
var defaultPlaceholderName = ".alist"
func getPlaceholderName(placeholder string) string {
//if placeholder == "" {
// return defaultPlaceholderName
//}
if placeholder == "" {
return defaultPlaceholderName
}
return placeholder
}
@ -88,7 +89,7 @@ func (d *S3) listV1(prefix string) ([]model.Obj, error) {
}
for _, object := range listObjectsResult.Contents {
name := path.Base(*object.Key)
if name == getPlaceholderName(d.Placeholder) {
if name == getPlaceholderName(d.Placeholder) || name == d.Placeholder {
continue
}
file := model.Object{
@ -140,7 +141,7 @@ func (d *S3) listV2(prefix string) ([]model.Obj, error) {
}
for _, object := range listObjectsResult.Contents {
name := path.Base(*object.Key)
if name == getPlaceholderName(d.Placeholder) {
if name == getPlaceholderName(d.Placeholder) || name == d.Placeholder {
continue
}
file := model.Object{
@ -222,11 +223,12 @@ func (d *S3) removeDir(ctx context.Context, src string) error {
}
}
_ = d.removeFile(path.Join(src, getPlaceholderName(d.Placeholder)))
_ = d.removeFile(path.Join(src, d.Placeholder))
return nil
}
func (d *S3) removeFile(src string) error {
key := getKey(src, true)
key := getKey(src, false)
input := &s3.DeleteObjectInput{
Bucket: &d.Bucket,
Key: &key,

205
drivers/smb/driver.go Normal file
View File

@ -0,0 +1,205 @@
package smb
import (
"context"
"errors"
"path/filepath"
"time"
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/hirochachacha/go-smb2"
)
type SMB struct {
model.Storage
Addition
fs *smb2.Share
lastConnTime time.Time
}
func (d *SMB) Config() driver.Config {
return config
}
func (d *SMB) GetAddition() driver.Additional {
return d.Addition
}
func (d *SMB) Init(ctx context.Context, storage model.Storage) error {
d.Storage = storage
err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition)
if err != nil {
return err
}
return d.initFS()
}
func (d *SMB) Drop(ctx context.Context) error {
if d.fs != nil {
_ = d.fs.Umount()
}
return nil
}
func (d *SMB) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
if err := d.checkConn(); err != nil {
return nil, err
}
fullPath := d.getSMBPath(dir)
rawFiles, err := d.fs.ReadDir(fullPath)
if err != nil {
d.cleanLastConnTime()
return nil, err
}
d.updateLastConnTime()
var files []model.Obj
for _, f := range rawFiles {
file := model.ObjThumb{
Object: model.Object{
Name: f.Name(),
Modified: f.ModTime(),
Size: f.Size(),
IsFolder: f.IsDir(),
},
}
files = append(files, &file)
}
return files, nil
}
//func (d *SMB) Get(ctx context.Context, path string) (model.Obj, error) {
// // this is optional
// return nil, errs.NotImplement
//}
func (d *SMB) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
if err := d.checkConn(); err != nil {
return nil, err
}
fullPath := d.getSMBPath(file)
remoteFile, err := d.fs.Open(fullPath)
if err != nil {
d.cleanLastConnTime()
return nil, err
}
d.updateLastConnTime()
return &model.Link{
Data: remoteFile,
}, nil
}
func (d *SMB) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
if err := d.checkConn(); err != nil {
return err
}
fullPath := filepath.Join(d.getSMBPath(parentDir), dirName)
err := d.fs.MkdirAll(fullPath, 0700)
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
return nil
}
func (d *SMB) Move(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.checkConn(); err != nil {
return err
}
srcPath := d.getSMBPath(srcObj)
dstPath := filepath.Join(d.getSMBPath(dstDir), srcObj.GetName())
err := d.fs.Rename(srcPath, dstPath)
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
return nil
}
func (d *SMB) Rename(ctx context.Context, srcObj model.Obj, newName string) error {
if err := d.checkConn(); err != nil {
return err
}
srcPath := d.getSMBPath(srcObj)
dstPath := filepath.Join(filepath.Dir(srcPath), newName)
err := d.fs.Rename(srcPath, dstPath)
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
return nil
}
func (d *SMB) Copy(ctx context.Context, srcObj, dstDir model.Obj) error {
if err := d.checkConn(); err != nil {
return err
}
srcPath := d.getSMBPath(srcObj)
dstPath := filepath.Join(d.getSMBPath(dstDir), srcObj.GetName())
var err error
if srcObj.IsDir() {
err = d.CopyDir(srcPath, dstPath)
} else {
err = d.CopyFile(srcPath, dstPath)
}
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
return nil
}
func (d *SMB) Remove(ctx context.Context, obj model.Obj) error {
if err := d.checkConn(); err != nil {
return err
}
var err error
fullPath := d.getSMBPath(obj)
if obj.IsDir() {
err = d.fs.RemoveAll(fullPath)
} else {
err = d.fs.Remove(fullPath)
}
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
return nil
}
func (d *SMB) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error {
if err := d.checkConn(); err != nil {
return err
}
fullPath := filepath.Join(d.getSMBPath(dstDir), stream.GetName())
out, err := d.fs.Create(fullPath)
if err != nil {
d.cleanLastConnTime()
return err
}
d.updateLastConnTime()
defer func() {
_ = out.Close()
if errors.Is(err, context.Canceled) {
_ = d.fs.Remove(fullPath)
}
}()
err = utils.CopyWithCtx(ctx, out, stream, stream.GetSize(), up)
if err != nil {
return err
}
return nil
}
//func (d *SMB) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) {
// return nil, errs.NotSupport
//}
var _ driver.Driver = (*SMB)(nil)

28
drivers/smb/meta.go Normal file
View File

@ -0,0 +1,28 @@
package smb
import (
"github.com/alist-org/alist/v3/internal/driver"
"github.com/alist-org/alist/v3/internal/op"
)
type Addition struct {
driver.RootPath
Address string `json:"address" required:"true"`
Username string `json:"username" required:"true"`
Password string `json:"password"`
ShareName string `json:"share_name" required:"true"`
}
var config = driver.Config{
Name: "SMB",
LocalSort: true,
OnlyLocal: true,
DefaultRoot: ".",
NoCache: true,
}
func init() {
op.RegisterDriver(config, func() driver.Driver {
return &SMB{}
})
}

1
drivers/smb/types.go Normal file
View File

@ -0,0 +1 @@
package smb

142
drivers/smb/util.go Normal file
View File

@ -0,0 +1,142 @@
package smb
import (
"io"
"io/fs"
"net"
"os"
"path/filepath"
"time"
"github.com/alist-org/alist/v3/internal/model"
"github.com/hirochachacha/go-smb2"
)
func (d *SMB) updateLastConnTime() {
d.lastConnTime = time.Now()
}
func (d *SMB) cleanLastConnTime() {
d.lastConnTime = time.Now().AddDate(0, 0, -1)
}
func (d *SMB) initFS() error {
conn, err := net.Dial("tcp", d.Address)
if err != nil {
return err
}
dialer := &smb2.Dialer{
Initiator: &smb2.NTLMInitiator{
User: d.Username,
Password: d.Password,
},
}
s, err := dialer.Dial(conn)
if err != nil {
return err
}
d.fs, err = s.Mount(d.ShareName)
if err != nil {
return err
}
d.updateLastConnTime()
return err
}
func (d *SMB) checkConn() error {
if time.Since(d.lastConnTime) < 5*time.Minute {
return nil
}
if d.fs != nil {
_ = d.fs.Umount()
}
return d.initFS()
}
func (d *SMB) getSMBPath(dir model.Obj) string {
fullPath := dir.GetPath()
if fullPath[0:1] != "." {
fullPath = "." + fullPath
}
return fullPath
}
// CopyFile File copies a single file from src to dst
func (d *SMB) CopyFile(src, dst string) error {
var err error
var srcfd *smb2.File
var dstfd *smb2.File
var srcinfo fs.FileInfo
if srcfd, err = d.fs.Open(src); err != nil {
return err
}
defer srcfd.Close()
if dstfd, err = d.CreateNestedFile(dst); err != nil {
return err
}
defer dstfd.Close()
if _, err = io.Copy(dstfd, srcfd); err != nil {
return err
}
if srcinfo, err = d.fs.Stat(src); err != nil {
return err
}
return d.fs.Chmod(dst, srcinfo.Mode())
}
// CopyDir Dir copies a whole directory recursively
func (d *SMB) CopyDir(src string, dst string) error {
var err error
var fds []fs.FileInfo
var srcinfo fs.FileInfo
if srcinfo, err = d.fs.Stat(src); err != nil {
return err
}
if err = d.fs.MkdirAll(dst, srcinfo.Mode()); err != nil {
return err
}
if fds, err = d.fs.ReadDir(src); err != nil {
return err
}
for _, fd := range fds {
srcfp := filepath.Join(src, fd.Name())
dstfp := filepath.Join(dst, fd.Name())
if fd.IsDir() {
if err = d.CopyDir(srcfp, dstfp); err != nil {
return err
}
} else {
if err = d.CopyFile(srcfp, dstfp); err != nil {
return err
}
}
}
return nil
}
// Exists determine whether the file exists
func (d *SMB) Exists(name string) bool {
if _, err := d.fs.Stat(name); err != nil {
if os.IsNotExist(err) {
return false
}
}
return true
}
// CreateNestedFile create nested file
func (d *SMB) CreateNestedFile(path string) (*smb2.File, error) {
basePath := filepath.Dir(path)
if !d.Exists(basePath) {
err := d.fs.MkdirAll(basePath, 0700)
if err != nil {
return nil, err
}
}
return d.fs.Create(path)
}

View File

@ -59,13 +59,18 @@ func (x *Thunder) Init(ctx context.Context, storage model.Storage) (err error) {
"j",
"4scKJNdd7F27Hv7tbt",
},
DeviceID: "9aa5c268e7bcfc197a9ad88e2fb330e5",
DeviceID: utils.GetMD5Encode(x.Username + x.Password),
ClientID: "Xp6vsxz_7IYVw2BB",
ClientSecret: "Xp6vsy4tN9toTVdMSpomVdXpRmES",
ClientVersion: "7.51.0.8196",
PackageName: "com.xunlei.downloadprovider",
UserAgent: "ANDROID-com.xunlei.downloadprovider/7.51.0.8196 netWorkType/5G appid/40 deviceName/Xiaomi_M2004j7ac deviceModel/M2004J7AC OSVersion/12 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_14_186-perf-gddfs8vbb238b) (JAVA 0)",
DownloadUserAgent: "Dalvik/2.1.0 (Linux; U; Android 12; M2004J7AC Build/SP1A.210812.016)",
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
refreshTokenFunc: func() error {
// 通过RefreshToken刷新
@ -88,7 +93,6 @@ func (x *Thunder) Init(ctx context.Context, storage model.Storage) (err error) {
ctoekn := strings.TrimSpace(x.CaptchaToken)
if ctoekn != "" {
x.SetCaptchaToken(ctoekn)
x.CaptchaToken = ""
}
// 防止重复登录
@ -139,7 +143,12 @@ func (x *ThunderExpert) Init(ctx context.Context, storage model.Storage) (err er
Common: &Common{
client: base.NewRestyClient(),
DeviceID: x.DeviceID,
DeviceID: func() string {
if len(x.DeviceID) != 32 {
return utils.GetMD5Encode(x.DeviceID)
}
return x.DeviceID
}(),
ClientID: x.ClientID,
ClientSecret: x.ClientSecret,
ClientVersion: x.ClientVersion,
@ -147,12 +156,16 @@ func (x *ThunderExpert) Init(ctx context.Context, storage model.Storage) (err er
UserAgent: x.UserAgent,
DownloadUserAgent: x.DownloadUserAgent,
UseVideoUrl: x.UseVideoUrl,
refreshCTokenCk: func(token string) {
x.CaptchaToken = token
op.MustSaveDriverStorage(x)
},
},
}
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
// 签名方法
@ -206,7 +219,6 @@ func (x *ThunderExpert) Init(ctx context.Context, storage model.Storage) (err er
// 仅修改验证码token
if x.CaptchaToken != "" {
x.SetCaptchaToken(x.CaptchaToken)
x.CaptchaToken = ""
}
x.XunLeiCommon.UserAgent = x.UserAgent
x.XunLeiCommon.DownloadUserAgent = x.DownloadUserAgent

View File

@ -53,6 +53,9 @@ type Common struct {
UserAgent string
DownloadUserAgent string
UseVideoUrl bool
// 验证码token刷新成功回调
refreshCTokenCk func(token string)
}
func (c *Common) SetCaptchaToken(captchaToken string) {
@ -125,13 +128,16 @@ func (c *Common) refreshCaptchaToken(action string, metas map[string]string) err
}
if resp.Url != "" {
return fmt.Errorf("need verify:%s", resp.Url)
return fmt.Errorf(`need verify: <a target="_blank" href="%s">Click Here</a>`, resp.Url)
}
if resp.CaptchaToken == "" {
return fmt.Errorf("empty captchaToken")
}
if c.refreshCTokenCk != nil {
c.refreshCTokenCk(resp.CaptchaToken)
}
c.SetCaptchaToken(resp.CaptchaToken)
return nil
}

View File

@ -119,7 +119,7 @@ func (ca *CookieAuth) getSPCookie(conf *SuccessResponse) (CookieResponse, error)
Jar: jar,
}
// Send the previously aquired Token as a Post parameter
// Send the previously acquired Token as a Post parameter
if _, err = client.Post(u.String(), "text/xml", strings.NewReader(conf.Succ.Token)); err != nil {
return CookieResponse{}, err
}

35
go.mod
View File

@ -1,23 +1,33 @@
module github.com/alist-org/alist/v3
go 1.18
go 1.19
require (
github.com/SheltonZhu/115driver v1.0.12
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a
github.com/aws/aws-sdk-go v1.44.88
github.com/caarlos0/env/v6 v6.9.3
github.com/disintegration/imaging v1.6.2
github.com/gin-contrib/cors v1.3.1
github.com/gin-gonic/gin v1.8.0
github.com/gin-gonic/gin v1.8.1
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-jwt/jwt/v4 v4.4.2
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0
github.com/hirochachacha/go-smb2 v1.1.0
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4
github.com/json-iterator/go v1.1.12
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.5
github.com/pquerna/otp v1.3.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf
github.com/upyun/go-sdk/v3 v3.0.3
github.com/winfsp/cgofuse v1.5.0
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b
gorm.io/driver/mysql v1.3.4
gorm.io/driver/postgres v1.3.7
gorm.io/driver/sqlite v1.3.4
@ -25,13 +35,17 @@ require (
)
require (
github.com/aws/aws-sdk-go v1.44.88 // indirect
github.com/aead/ecdh v0.2.0 // indirect
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible // indirect
github.com/andreburgaud/crypt2go v1.1.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/gaoyb7/115drive-webdav v0.1.8 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
@ -47,24 +61,23 @@ require (
github.com/jackc/pgx/v4 v4.16.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.13 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/sftp v1.13.5 // indirect
github.com/pierrec/lz4/v4 v4.1.17 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/upyun/go-sdk/v3 v3.0.3 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 // indirect
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b // indirect
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

64
go.sum
View File

@ -2,10 +2,20 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/SheltonZhu/115driver v1.0.12 h1:+GlIM5h8tzuec6MzK0wFwb7bY77nav7JhY8lTljzls4=
github.com/SheltonZhu/115driver v1.0.12/go.mod h1:00ixivHH5HqDj4S7kAWbkuUrjtsJTxc7cGv5RMw3RVs=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a h1:RenIAa2q4H8UcS/cqmwdT1WCWIAH5aumP8m8RpbqVsE=
github.com/Xhofe/go-cache v0.0.0-20220723083548-714439c8af9a/go.mod h1:sSBbaOg90XwWKtpT56kVujF0bIeVITnPlssLclogS04=
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible h1:QoRMR0TCctLDqBCMyOu1eXdZyMw3F7uGA9qPn2J4+R8=
github.com/aliyun/aliyun-oss-go-sdk v2.2.5+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andreburgaud/crypt2go v1.1.0 h1:eitZxTPY1krUsxinsng3Qvt/Ud7q/aQmmYRh8p4hyPw=
github.com/andreburgaud/crypt2go v1.1.0/go.mod h1:4qhZPzarj1dCIRmCkpdgCklwp+hBq9yEt0zPe9Ayuhc=
github.com/aws/aws-sdk-go v1.44.88 h1:9jhiZsTx9koQQsM29RTgwI0g4mfyphCdc3bkUcKrdwA=
github.com/aws/aws-sdk-go v1.44.88/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caarlos0/env/v6 v6.9.3 h1:Tyg69hoVXDnpO5Qvpsu8EoquarbPyQb+YwExWHP8wWU=
@ -22,13 +32,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/gaoyb7/115drive-webdav v0.1.8 h1:EJt4PSmcbvBY4KUh2zSo5p6fN9LZFNkIzuKejipubVw=
github.com/gaoyb7/115drive-webdav v0.1.8/go.mod h1:BKbeY6j8SKs3+rzBFFALznGxbPmefEm3vA+dGhqgOGU=
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA=
github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
github.com/gin-gonic/gin v1.8.0 h1:4WFH5yycBMA3za5Hnl425yd9ymdw1XPm4666oab+hv4=
github.com/gin-gonic/gin v1.8.0/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
@ -39,6 +53,7 @@ github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
@ -67,6 +82,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hirochachacha/go-smb2 v1.1.0 h1:b6hs9qKIql9eVXAiN0M2wSFY5xnhbHAQoCwRKbaRTZI=
github.com/hirochachacha/go-smb2 v1.1.0/go.mod h1:8F1A4d5EZzrGu5R7PU163UcMRDJQl4FtcxjBfsY8TZE=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
@ -125,6 +142,7 @@ github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4 h1:8bWaY08VCoFn17gezY
github.com/jlaffaye/ftp v0.0.0-20220829015825-b85cf1edccd4/go.mod h1:hhq4G4crv+nW2qXtNYcuzLeOudG92Ps37HEKeg2e3lE=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -157,8 +175,9 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
@ -170,8 +189,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77 h1:dg/EaaJLPIg4xn2kaZil7Ax3wfoxcFXaBwyOTlcz5AI=
github.com/orzogc/fake115uploader v0.3.3-0.20221009101310-08b764073b77/go.mod h1:FD9a09Vw07CSMTdT0Y7ttStOa1WZsnPBslliMw2DkeM=
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc=
github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -198,6 +221,8 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@ -211,9 +236,11 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf h1:Y43S3e9P1NPs/QF4R5/SdlXj2d31540hP4Gk8VKNvDg=
github.com/t3rm1n4l/go-mega v0.0.0-20220725095014-c4e0c2b5debf/go.mod h1:c+cGNU1qi9bO7ZF4IRMYk+KaZTNiQ/gQrSbyMmGFq1Q=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
@ -221,6 +248,7 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/upyun/go-sdk/v3 v3.0.3 h1:2wUkNk2fyJReMYHMvJyav050D83rYwSjN7mEPR0Pp8Q=
github.com/upyun/go-sdk/v3 v3.0.3/go.mod h1:P/SnuuwhrIgAVRd/ZpzDWqCsBAf/oHg7UggbAxyZa0E=
github.com/valyala/fastjson v1.6.3 h1:tAKFnnwmeMGPbwJ7IwxcTPCNr3uIzoIj3/Fh90ra4xc=
github.com/winfsp/cgofuse v1.5.0 h1:MsBP7Mi/LiJf/7/F3O/7HjjR009ds6KCdqXzKpZSWxI=
github.com/winfsp/cgofuse v1.5.0/go.mod h1:h3awhoUOcn2VYVKCwDaYxSLlZwnyK+A8KaDoLUp2lbU=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -235,21 +263,23 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539 h1:/eM0PCrQI2xd471rI+snWuu251/+/jpBpZqir2mPdnU=
golang.org/x/image v0.0.0-20220722155232-062f8c9fd539/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
@ -264,8 +294,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b h1:ZmngSVLe/wycRns9MKikG9OWIEjGcGAkacif7oYQaUY=
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -285,14 +313,13 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -301,6 +328,9 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -334,9 +364,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.3.4 h1:/KoBMgsUHC3bExsekDcmNYaBnfH2WNeFuXqqrqMc98Q=
gorm.io/driver/mysql v1.3.4/go.mod h1:s4Tq0KmD0yhPGHbZEwg1VPlH0vT/GBHJZorPzhcxBUE=
gorm.io/driver/postgres v1.3.7 h1:FKF6sIMDHDEvvMF/XJvbnCl0nu6KSKUaPXevJ4r+VYQ=

View File

@ -2,7 +2,6 @@ package aria2
import (
"fmt"
"mime"
"os"
"path"
"strconv"
@ -13,6 +12,7 @@ import (
"github.com/alist-org/alist/v3/internal/model"
"github.com/alist-org/alist/v3/internal/op"
"github.com/alist-org/alist/v3/pkg/task"
"github.com/alist-org/alist/v3/pkg/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@ -70,6 +70,8 @@ func (m *Monitor) Update() (bool, error) {
info, err := client.TellStatus(m.tsk.ID)
if err != nil {
m.retried++
log.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
return false, nil
}
if m.retried > 5 {
return true, errors.Errorf("failed to get status of %s, retried %d times", m.tsk.ID, m.retried)
@ -103,7 +105,14 @@ func (m *Monitor) Update() (bool, error) {
return true, errors.WithMessage(err, "failed to transfer file")
case "error":
return true, errors.Errorf("failed to download %s, error: %s", m.tsk.ID, info.ErrorMessage)
case "active", "waiting", "paused":
case "active":
m.tsk.SetStatus("aria2: " + info.Status)
if info.Seeder == "true" {
err := m.Complete()
return true, errors.WithMessage(err, "failed to transfer file")
}
return false, nil
case "waiting", "paused":
m.tsk.SetStatus("aria2: " + info.Status)
return false, nil
case "removed":
@ -147,10 +156,7 @@ func (m *Monitor) Complete() error {
Func: func(tsk *task.Task[uint64]) error {
defer wg.Done()
size, _ := strconv.ParseInt(file.Length, 10, 64)
mimetype := mime.TypeByExtension(path.Ext(file.Path))
if mimetype == "" {
mimetype = "application/octet-stream"
}
mimetype := utils.GetMimeType(file.Path)
f, err := os.Open(file.Path)
if err != nil {
return errors.Wrapf(err, "failed to open file %s", file.Path)

View File

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

View File

@ -1,7 +1,6 @@
package bootstrap
import (
"io/ioutil"
"os"
"path/filepath"
@ -13,19 +12,28 @@ import (
)
func InitConfig() {
log.Infof("reading config file: %s", flags.Config)
if !utils.Exists(flags.Config) {
if flags.ForceBinDir {
ex, err := os.Executable()
if err != nil {
utils.Log.Fatal(err)
}
exPath := filepath.Dir(ex)
flags.DataDir = filepath.Join(exPath, "data")
}
configPath := filepath.Join(flags.DataDir, "config.json")
log.Infof("reading config file: %s", configPath)
if !utils.Exists(configPath) {
log.Infof("config file not exists, creating default config file")
_, err := utils.CreateNestedFile(flags.Config)
_, err := utils.CreateNestedFile(configPath)
if err != nil {
log.Fatalf("failed to create config file: %+v", err)
}
conf.Conf = conf.DefaultConfig()
if !utils.WriteJsonToFile(flags.Config, conf.Conf) {
if !utils.WriteJsonToFile(configPath, conf.Conf) {
log.Fatalf("failed to create default config file")
}
} else {
configBytes, err := ioutil.ReadFile(flags.Config)
configBytes, err := os.ReadFile(configPath)
if err != nil {
log.Fatalf("reading config file error: %+v", err)
}
@ -39,7 +47,7 @@ func InitConfig() {
if err != nil {
log.Fatalf("marshal config error: %+v", err)
}
err = ioutil.WriteFile(flags.Config, confBody, 0777)
err = os.WriteFile(configPath, confBody, 0777)
if err != nil {
log.Fatalf("update config struct error: %+v", err)
}
@ -59,7 +67,7 @@ func InitConfig() {
if err != nil {
log.Errorln("failed delete temp file:", err)
}
err = os.MkdirAll(conf.Conf.TempDir, 0700)
err = os.MkdirAll(conf.Conf.TempDir, 0777)
if err != nil {
log.Fatalf("create temp dir error: %+v", err)
}

View File

@ -17,7 +17,7 @@ func initDevData() {
Order: 0,
Driver: "Local",
Status: "",
Addition: `{"root_folder":"."}`,
Addition: `{"root_folder_path":"."}`,
})
if err != nil {
log.Fatalf("failed to create storage: %+v", err)

View File

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

View File

@ -1,6 +1,8 @@
package data
import (
"os"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/internal/db"
"github.com/alist-org/alist/v3/internal/model"
@ -13,8 +15,11 @@ import (
func initUser() {
admin, err := db.GetAdmin()
adminPassword := random.String(8)
envpass := os.Getenv("ALIST_ADMIN_PASSWORD")
if flags.Dev {
adminPassword = "admin"
} else if len(envpass) > 0 {
adminPassword = envpass
}
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {

View File

@ -1,6 +1,9 @@
package conf
import (
"path/filepath"
"github.com/alist-org/alist/v3/cmd/flags"
"github.com/alist-org/alist/v3/pkg/utils/random"
)
@ -23,7 +26,7 @@ type Scheme struct {
}
type LogConfig struct {
Enable bool `json:"enable" env:"log_enable"`
Enable bool `json:"enable" env:"LOG_ENABLE"`
Name string `json:"name" env:"LOG_NAME"`
MaxSize int `json:"max_size" env:"MAX_SIZE"`
MaxBackups int `json:"max_backups" env:"MAX_BACKUPS"`
@ -32,35 +35,38 @@ type LogConfig struct {
}
type Config struct {
Force bool `json:"force"`
Address string `json:"address" env:"ADDR"`
Port int `json:"port" env:"PORT"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
// CaCheExpiration int `json:"cache_expiration" env:"CACHE_EXPIRATION"`
Cdn string `json:"cdn" env:"CDN"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
Log LogConfig `json:"log"`
Force bool `json:"force" env:"FORCE"`
Address string `json:"address" env:"ADDR"`
Port int `json:"port" env:"PORT"`
SiteURL string `json:"site_url" env:"SITE_URL"`
Cdn string `json:"cdn" env:"CDN"`
JwtSecret string `json:"jwt_secret" env:"JWT_SECRET"`
TokenExpiresIn int `json:"token_expires_in" env:"TOKEN_EXPIRES_IN"`
Database Database `json:"database"`
Scheme Scheme `json:"scheme"`
TempDir string `json:"temp_dir" env:"TEMP_DIR"`
Log LogConfig `json:"log"`
}
func DefaultConfig() *Config {
tempDir := filepath.Join(flags.DataDir, "temp")
logPath := filepath.Join(flags.DataDir, "log/log.log")
dbPath := filepath.Join(flags.DataDir, "data.db")
return &Config{
Address: "0.0.0.0",
Port: 5244,
JwtSecret: random.String(16),
Cdn: "",
TempDir: "data/temp",
Address: "0.0.0.0",
Port: 5244,
JwtSecret: random.String(16),
TokenExpiresIn: 48,
TempDir: tempDir,
Database: Database{
Type: "sqlite3",
Port: 0,
TablePrefix: "x_",
DBFile: "data/data.db",
DBFile: dbPath,
},
// CaCheExpiration: 30,
Log: LogConfig{
Enable: true,
Name: "log/log.log",
Name: logPath,
MaxSize: 10,
MaxBackups: 5,
MaxAge: 28,

View File

@ -3,7 +3,6 @@ package fs
import (
"fmt"
"io"
"mime"
"net/http"
"os"
stdpath "path"
@ -38,7 +37,7 @@ var httpClient = &http.Client{}
func getFileStreamFromLink(file model.Obj, link *model.Link) (model.FileStreamer, error) {
var rc io.ReadCloser
mimetype := mime.TypeByExtension(stdpath.Ext(file.GetName()))
mimetype := utils.GetMimeType(file.GetName())
if link.Data != nil {
rc = link.Data
} else if link.FilePath != nil {

View File

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

View File

@ -38,6 +38,13 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
}
path = utils.StandardizePath(path)
log.Debugf("op.List %s", path)
key := Key(storage, path)
if len(refresh) == 0 || !refresh[0] {
if files, ok := listCache.Get(key); ok {
log.Debugf("use cache when list %s", path)
return files, nil
}
}
dir, err := Get(ctx, storage, path)
if err != nil {
return nil, errors.WithMessage(err, "failed get dir")
@ -46,22 +53,18 @@ func List(ctx context.Context, storage driver.Driver, path string, args model.Li
if !dir.IsDir() {
return nil, errors.WithStack(errs.NotFolder)
}
if storage.Config().NoCache {
objs, err := storage.List(ctx, dir, args)
return objs, errors.WithStack(err)
}
key := Key(storage, path)
if len(refresh) == 0 || !refresh[0] {
if files, ok := listCache.Get(key); ok && len(files) > 0 {
return files, nil
}
}
objs, err, _ := listG.Do(key, func() ([]model.Obj, error) {
files, err := storage.List(ctx, dir, args)
if err != nil {
return nil, errors.Wrapf(err, "failed to list objs")
}
listCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
if !storage.Config().NoCache {
if len(files) > 0 {
listCache.Set(key, files, cache.WithEx[[]model.Obj](time.Minute*time.Duration(storage.GetStorage().CacheExpiration)))
} else {
listCache.Del(key)
}
}
return files, nil
})
return objs, err
@ -128,6 +131,7 @@ func Get(ctx context.Context, storage driver.Driver, path string) (model.Obj, er
return f, nil
}
}
log.Debugf("cant find obj with name: %s", name)
return nil, errors.WithStack(errs.ObjectNotFound)
}
@ -181,42 +185,49 @@ func Other(ctx context.Context, storage driver.Driver, args model.FsOtherArgs) (
}
}
var mkdirG singleflight.Group[interface{}]
func MakeDir(ctx context.Context, storage driver.Driver, path string) error {
if storage.Config().CheckStatus && storage.GetStorage().Status != WORK {
return errors.Errorf("storage not init: %s", storage.GetStorage().Status)
}
path = utils.StandardizePath(path)
// check if dir exists
f, err := Get(ctx, storage, path)
if err != nil {
if errs.IsObjectNotFound(err) {
parentPath, dirName := stdpath.Split(path)
err = MakeDir(ctx, storage, parentPath)
if err != nil {
return errors.WithMessagef(err, "failed to make parent dir [%s]", parentPath)
key := Key(storage, path)
_, err, _ := mkdirG.Do(key, func() (interface{}, error) {
// check if dir exists
f, err := Get(ctx, storage, path)
if err != nil {
if errs.IsObjectNotFound(err) {
parentPath, dirName := stdpath.Split(path)
err = MakeDir(ctx, storage, parentPath)
if err != nil {
return nil, errors.WithMessagef(err, "failed to make parent dir [%s]", parentPath)
}
parentDir, err := Get(ctx, storage, parentPath)
// this should not happen
if err != nil {
return nil, errors.WithMessagef(err, "failed to get parent dir [%s]", parentPath)
}
err = storage.MakeDir(ctx, parentDir, dirName)
if err == nil {
ClearCache(storage, parentPath)
}
return nil, errors.WithStack(err)
} else {
return nil, errors.WithMessage(err, "failed to check if dir exists")
}
parentDir, err := Get(ctx, storage, parentPath)
// this should not happen
if err != nil {
return errors.WithMessagef(err, "failed to get parent dir [%s]", parentPath)
}
err = storage.MakeDir(ctx, parentDir, dirName)
if err == nil {
ClearCache(storage, parentPath)
}
return errors.WithStack(err)
} else {
return errors.WithMessage(err, "failed to check if dir exists")
// dir exists
if f.IsDir() {
return nil, nil
} else {
// dir to make is a file
return nil, errors.New("file exists")
}
}
} else {
// dir exists
if f.IsDir() {
return nil
} else {
// dir to make is a file
return errors.New("file exists")
}
}
})
return err
}
func Move(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string) error {

View File

@ -182,13 +182,15 @@ func DeleteStorageById(ctx context.Context, id uint) error {
if err != nil {
return errors.WithMessage(err, "failed get storage")
}
storageDriver, err := GetStorageByVirtualPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrapf(err, "failed drop storage")
if !storage.Disabled {
storageDriver, err := GetStorageByVirtualPath(storage.MountPath)
if err != nil {
return errors.WithMessage(err, "failed get storage driver")
}
// drop the storage in the driver
if err := storageDriver.Drop(ctx); err != nil {
return errors.Wrapf(err, "failed drop storage")
}
}
// delete the storage in the database
if err := db.DeleteStorageById(id); err != nil {

View File

@ -191,7 +191,7 @@ func (id *Client) PauseAll() (g string, err error)
`aria2.pauseAll()` This method is equal to calling `aria2.pause()` for every active/waiting download. This methods returns OK for success.
```
func (id *Client) PurgeDowloadResult() (g string, err error)
func (id *Client) PurgeDownloadResult() (g string, err error)
```
`aria2.purgeDownloadResult()` This method purges completed/error/removed downloads to free memory. This method returns OK.
@ -254,4 +254,4 @@ func (id *Client) Unpause(gid string) (g string, err error)
func (id *Client) UnpauseAll() (g string, err error)
```
`aria2.unpauseAll()` This method is equal to calling `aria2.unpause()` for every active/waiting download. This methods returns OK for success.
`aria2.unpauseAll()` This method is equal to calling `aria2.unpause()` for every active/waiting download. This methods returns OK for success.

View File

@ -28,7 +28,7 @@ type httpCaller struct {
once sync.Once
}
func newHTTPCaller(ctx context.Context, u *url.URL, timeout time.Duration, notifer Notifier) *httpCaller {
func newHTTPCaller(ctx context.Context, u *url.URL, timeout time.Duration, notifier Notifier) *httpCaller {
c := &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: 1,
@ -45,8 +45,8 @@ func newHTTPCaller(ctx context.Context, u *url.URL, timeout time.Duration, notif
var wg sync.WaitGroup
ctx, cancel := context.WithCancel(ctx)
h := &httpCaller{uri: u.String(), c: c, cancel: cancel, wg: &wg}
if notifer != nil {
h.setNotifier(ctx, *u, notifer)
if notifier != nil {
h.setNotifier(ctx, *u, notifier)
}
return h
}
@ -59,7 +59,7 @@ func (h *httpCaller) Close() (err error) {
return
}
func (h *httpCaller) setNotifier(ctx context.Context, u url.URL, notifer Notifier) (err error) {
func (h *httpCaller) setNotifier(ctx context.Context, u url.URL, notifier Notifier) (err error) {
u.Scheme = "ws"
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
@ -101,17 +101,17 @@ func (h *httpCaller) setNotifier(ctx context.Context, u url.URL, notifer Notifie
}
switch request.Method {
case "aria2.onDownloadStart":
notifer.OnDownloadStart(request.Params)
notifier.OnDownloadStart(request.Params)
case "aria2.onDownloadPause":
notifer.OnDownloadPause(request.Params)
notifier.OnDownloadPause(request.Params)
case "aria2.onDownloadStop":
notifer.OnDownloadStop(request.Params)
notifier.OnDownloadStop(request.Params)
case "aria2.onDownloadComplete":
notifer.OnDownloadComplete(request.Params)
notifier.OnDownloadComplete(request.Params)
case "aria2.onDownloadError":
notifer.OnDownloadError(request.Params)
notifier.OnDownloadError(request.Params)
case "aria2.onBtDownloadComplete":
notifer.OnBtDownloadComplete(request.Params)
notifier.OnBtDownloadComplete(request.Params)
default:
log.Printf("unexpected notification: %s", request.Method)
}

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

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

View File

@ -17,7 +17,7 @@ func TestJoin(t *testing.T) {
func eq(t *testing.T, expected string, s0 string, s1 string) {
s := Join(s0, s1)
if s != expected {
t.Error("For", "'"+s0+"','"+s1+"'", "expeted", "'"+expected+"'", "got", "'"+s+"'")
t.Error("For", "'"+s0+"','"+s1+"'", "expected", "'"+expected+"'", "got", "'"+s+"'")
}
}

9
pkg/utils/email.go Normal file
View File

@ -0,0 +1,9 @@
package utils
import "regexp"
func IsEmailFormat(email string) bool {
pattern := `^[0-9a-z][_.0-9a-z-]{0,31}@([0-9a-z][0-9a-z-]{0,30}[0-9a-z]\.){1,4}[a-z]{2,4}$`
reg := regexp.MustCompile(pattern)
return reg.MatchString(email)
}

View File

@ -4,6 +4,7 @@ import (
"fmt"
"io"
"io/ioutil"
"mime"
"os"
"path"
"path/filepath"
@ -87,7 +88,7 @@ func CreateNestedFile(path string) (*os.File, error) {
if !Exists(basePath) {
err := os.MkdirAll(basePath, 0700)
if err != nil {
log.Errorf("can't create foler%s", err)
log.Errorf("can't create folder, %s", err)
return nil, err
}
}
@ -136,3 +137,12 @@ func GetFileType(filename string) int {
}
return conf.UNKNOWN
}
func GetMimeType(name string) string {
ext := path.Ext(name)
m := mime.TypeByExtension(ext)
if m != "" {
return m
}
return "application/octet-stream"
}

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