mirror of https://github.com/Aruelius/cloud189
add many new features
This commit is contained in:
parent
d9521cbab6
commit
987106bb26
|
@ -0,0 +1,138 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# other
|
||||
.config
|
||||
.directory
|
||||
.idea/
|
||||
*.log
|
||||
|
||||
/downloads
|
||||
/bak
|
316
189.py
316
189.py
|
@ -1,316 +0,0 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
import rsa
|
||||
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
||||
|
||||
session = requests.session()
|
||||
session.headers.update({
|
||||
'Referer': 'https://open.e.189.cn/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36'
|
||||
})
|
||||
|
||||
RSA_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDY7mpaUysvgQkbp0iIn2ezoUyh
|
||||
i1zPFn0HCXloLFWT7uoNkqtrphpQ/63LEcPz1VYzmDuDIf3iGxQKzeoHTiVMSmW6
|
||||
FlhDeqVOG094hFJvZeK4OzA6HVwzwnEW5vIZ7d+u61RV1bsFxmB68+8JXs3ycGcE
|
||||
4anY+YzZJcyOcEGKVQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
def encrypt(password: str) -> str:
|
||||
return base64.b64encode(
|
||||
rsa.encrypt(
|
||||
(password).encode('utf-8'),
|
||||
rsa.PublicKey.load_pkcs1_openssl_pem(RSA_KEY.encode())
|
||||
)
|
||||
).decode()
|
||||
|
||||
b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
BI_RM = list("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
def int2char(a):
|
||||
return BI_RM[a]
|
||||
|
||||
def b64tohex(a):
|
||||
d = ""
|
||||
e = 0
|
||||
for i in range(len(a)):
|
||||
if list(a)[i] != "=":
|
||||
v = b64map.index(list(a)[i])
|
||||
if 0 == e:
|
||||
e = 1
|
||||
d += int2char(v >> 2)
|
||||
c = 3 & v
|
||||
elif 1 == e:
|
||||
e = 2
|
||||
d += int2char(c << 2 | v >> 4)
|
||||
c = 15 & v
|
||||
elif 2 == e:
|
||||
e = 3
|
||||
d += int2char(c)
|
||||
d += int2char(v >> 2)
|
||||
c = 3 & v
|
||||
else:
|
||||
e = 0
|
||||
d += int2char(c << 2 | v >> 4)
|
||||
d += int2char(15 & v)
|
||||
if e == 1:
|
||||
d += int2char(c << 2)
|
||||
# print(d)
|
||||
return d
|
||||
|
||||
def redirect():
|
||||
r = session.get("https://cloud.189.cn/udb/udb_login.jsp?pageId=1&redirectURL=/main.action")
|
||||
captchaToken = re.findall(r"captchaToken' value='(.+?)'", r.text)[0]
|
||||
lt = re.findall(r'lt = "(.+?)"', r.text)[0]
|
||||
returnUrl = re.findall(r"returnUrl = '(.+?)'", r.text)[0]
|
||||
paramId = re.findall(r'paramId = "(.+?)"', r.text)[0]
|
||||
session.headers.update({"lt": lt})
|
||||
return captchaToken, returnUrl, paramId
|
||||
|
||||
def md5(s):
|
||||
hl = hashlib.md5()
|
||||
hl.update(s.encode(encoding='utf-8'))
|
||||
return hl.hexdigest()
|
||||
|
||||
def needcaptcha(captchaToken):
|
||||
r = session.post(
|
||||
url="https://open.e.189.cn/api/logbox/oauth2/needcaptcha.do",
|
||||
data={
|
||||
"accountType": "01",
|
||||
"userName": "{RSA}" + b64tohex(encrypt(username)),
|
||||
"appKey": "cloud"
|
||||
}
|
||||
)
|
||||
if r.text == "0":
|
||||
print("DONT NEED CAPTCHA")
|
||||
return ""
|
||||
else:
|
||||
print("NEED CAPTCHA")
|
||||
r = session.get(
|
||||
url="https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do",
|
||||
params={"token": captchaToken}
|
||||
)
|
||||
with open("./captcha.png", "wb") as f:
|
||||
f.write(r.content)
|
||||
f.close()
|
||||
return input("验证码下载完成,打开 ./captcha.png 查看: ")
|
||||
|
||||
def save_cookie(username: str):
|
||||
with open(f"./.{username}", mode="w") as f:
|
||||
json.dump(session.cookies.get_dict(), f, indent=2)
|
||||
f.close()
|
||||
|
||||
def load_cookie(username: str):
|
||||
cookie_file = f"./.{username}"
|
||||
if os.path.exists(cookie_file):
|
||||
with open(cookie_file, mode="r") as f:
|
||||
cookie_dict = json.loads(f.read())
|
||||
f.close()
|
||||
[session.cookies.set(k, v, domain=".cloud.189.cn") for k, v in cookie_dict.items()]
|
||||
r = session.get("https://cloud.189.cn/v2/getUserLevelInfo.action")
|
||||
if "InvalidSessionKey" not in r.text: return True
|
||||
return False
|
||||
|
||||
def login():
|
||||
if load_cookie(username):
|
||||
return
|
||||
captchaToken, returnUrl, paramId = redirect()
|
||||
validateCode = needcaptcha(captchaToken)
|
||||
r = session.post(
|
||||
url="https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do",
|
||||
data={
|
||||
"appKey": "cloud",
|
||||
"accountType": '01',
|
||||
"userName": "{RSA}" + b64tohex(encrypt(username)),
|
||||
"password": "{RSA}" + b64tohex(encrypt(password)),
|
||||
"validateCode": validateCode,
|
||||
"captchaToken": captchaToken,
|
||||
"returnUrl": returnUrl,
|
||||
"mailSuffix": "@189.cn",
|
||||
"paramId": paramId
|
||||
}
|
||||
)
|
||||
msg = r.json()["msg"]
|
||||
if "登录成功" == msg:
|
||||
session.get(r.json()["toUrl"])
|
||||
save_cookie(username)
|
||||
else:
|
||||
print(msg)
|
||||
os._exit(0)
|
||||
|
||||
def get_file_size_str(filesize: int) -> str:
|
||||
if 0 < filesize < 1024**2:
|
||||
return f"{round(filesize/1024, 2)}KB"
|
||||
elif 1024**2 < filesize < 1024**3:
|
||||
return f"{round(filesize/1024**2, 2)}MB"
|
||||
elif 1024**3 < filesize < 1024**4:
|
||||
return f"{round(filesize/1024**3, 2)}GB"
|
||||
elif 1024**4 < filesize < 1024**5:
|
||||
return f"{round(filesize/1024**4, 2)}TB"
|
||||
else: return f"{filesize}Bytes"
|
||||
|
||||
def share_file(fileId):
|
||||
expireTime_dict = {
|
||||
"1": "1",
|
||||
"2": "7",
|
||||
"3": "2099"
|
||||
}
|
||||
expireTime = input("请选择分享有效期:1、1天,2、7天,3、永久:")
|
||||
withAccessCode = input("请选择分享形式:1、私密分享,2、公开分享:")
|
||||
if withAccessCode == "1":
|
||||
url = "https://cloud.189.cn/v2/privateLinkShare.action"
|
||||
params = {
|
||||
"fileId": fileId,
|
||||
"expireTime": expireTime_dict[expireTime],
|
||||
"withAccessCode": withAccessCode
|
||||
}
|
||||
else:
|
||||
url = "https://cloud.189.cn/v2/createOutLinkShare.action"
|
||||
params = {
|
||||
"fileId": fileId,
|
||||
"expireTime": expireTime_dict[expireTime]
|
||||
}
|
||||
r = session.get(url=url, params=params).json()
|
||||
msg = f"链接:{r['shortShareUrl']} "
|
||||
msg += "" if not r.get("accessCode") else f"访问码:{r['accessCode']}"
|
||||
print(msg)
|
||||
|
||||
def get_files():
|
||||
r = session.get(
|
||||
url="https://cloud.189.cn/v2/listFiles.action",
|
||||
params={
|
||||
"fileId": "-11", # 根目录
|
||||
"inGroupSpace": "false",
|
||||
"orderBy": "1",
|
||||
"order": "ASC",
|
||||
"pageNum": "1",
|
||||
"pageSize": "60"
|
||||
}
|
||||
).json()
|
||||
for file in r["data"]:
|
||||
folder_or_file = "文件夹: " if file["isFolder"] else f"大小: {get_file_size_str(file['fileSize'])} 文件名: "
|
||||
filename = file["fileName"]
|
||||
print(f"{folder_or_file}{filename} {'' if file['isFolder'] else '文件ID: ' + file['fileId']}")
|
||||
|
||||
def upload(filePath):
|
||||
session.headers["Referer"] = "https://cloud.189.cn"
|
||||
def get_upload_url():
|
||||
while True:
|
||||
try:
|
||||
r = session.post("https://cloud.189.cn/v2/getUserUploadUrl.action")
|
||||
return "https:" + r.json()["uploadUrl"]
|
||||
except:
|
||||
login()
|
||||
|
||||
def get_session_key():
|
||||
r = session.get(
|
||||
url="https://cloud.189.cn/main.action",
|
||||
headers={"Host": "cloud.189.cn"}
|
||||
)
|
||||
sessionKey = re.findall(r"sessionKey = '(.+?)'", r.text)[0]
|
||||
return sessionKey
|
||||
|
||||
def upload_file():
|
||||
filename = os.path.basename(filePath)
|
||||
filesize = os.path.getsize(filePath)
|
||||
print(f"正在上传: {filename} 大小: {get_file_size_str(filesize)}")
|
||||
upload_url = get_upload_url()
|
||||
multipart_data = MultipartEncoder(
|
||||
fields={
|
||||
"sessionKey": get_session_key(),
|
||||
"parentId": "-11", # 上传文件夹 根目录
|
||||
"albumId": "undefined",
|
||||
"opertype": "1",
|
||||
"fname": filename,
|
||||
'file': (filename, open(filePath, 'rb'), 'application/octet-stream')
|
||||
}
|
||||
)
|
||||
r = session.post(
|
||||
url=upload_url,
|
||||
data=multipart_data,
|
||||
headers={
|
||||
"Content-Type": multipart_data.content_type
|
||||
}
|
||||
).json()
|
||||
print(f"上传完毕!文件ID:{r['id']} 上传时间: {r['createDate']}")
|
||||
upload_file()
|
||||
|
||||
def task(task_type, fileid):
|
||||
taskInfos = []
|
||||
def get_file_info(download=False):
|
||||
r = session.get(f"https://cloud.189.cn/v2/getFileInfo.action?fileId={fileid}").json()
|
||||
filename = r["fileName"]
|
||||
if download:
|
||||
print(f"开始下载: {filename} 大小: {get_file_size_str(r['fileSize'])}")
|
||||
return "https:"+r["downloadUrl"], filename
|
||||
taskInfo = {}
|
||||
taskInfo["fileId"] = fileid
|
||||
taskInfo["srcParentId"] = "-11" # 根目录
|
||||
taskInfo["fileName"] = filename
|
||||
taskInfo["isFolder"] = 1 if r["isFolder"] else 0
|
||||
taskInfos.append(taskInfo)
|
||||
|
||||
def create_batch_task(action):
|
||||
r = session.post(
|
||||
url="https://cloud.189.cn/createBatchTask.action",
|
||||
data={
|
||||
"type": action,
|
||||
"taskInfos": json.dumps(taskInfos)
|
||||
}
|
||||
)
|
||||
if r.text:
|
||||
print("删除成功!")
|
||||
|
||||
def download(url, filename, filepath="./"):
|
||||
print(f"下载链接:{url}")
|
||||
if input("需要继续下载吗? 1、继续下载,2、取消下载") == "1":
|
||||
r = session.get(url, stream=True)
|
||||
with open(filepath+filename, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024**2):
|
||||
f.write(chunk)
|
||||
f.close()
|
||||
print(f"{filename} 下载完成!")
|
||||
|
||||
if task_type == "delete":
|
||||
get_file_info()
|
||||
create_batch_task("DELETE")
|
||||
elif task_type == "download":
|
||||
url, filename = get_file_info(True)
|
||||
download(url, filename)
|
||||
|
||||
if __name__ == "__main__":
|
||||
username = ""
|
||||
password = ""
|
||||
login()
|
||||
try:
|
||||
if sys.argv[1] == "upload":
|
||||
if os.path.isdir(sys.argv[2]):
|
||||
[upload(f"{os.path.abspath(sys.argv[2])}/{file}")
|
||||
for file in os.listdir(sys.argv[2])]
|
||||
elif os.path.isfile(sys.argv[2]):
|
||||
upload(sys.argv[2])
|
||||
else:
|
||||
print("未知类型文件")
|
||||
elif sys.argv[1] in ["delete", "download"]:
|
||||
task(sys.argv[1], int(sys.argv[2]))
|
||||
elif sys.argv[1] == "list":
|
||||
get_files()
|
||||
elif sys.argv[1] == "share":
|
||||
share_file(int(sys.argv[2]))
|
||||
except IndexError:
|
||||
print("请输入正确的参数:\n \
|
||||
upload [filename]: 上传文件\n \
|
||||
download [file id]: 下载文件\n \
|
||||
list: 列出根目录文件及文件夹\n \
|
||||
share: 分享文件\n \
|
||||
delete [file id]: 删除文件")
|
||||
except ValueError:
|
||||
print("文件ID输入错误")
|
56
README.md
56
README.md
|
@ -1,39 +1,45 @@
|
|||
# Cloud189 CLI
|
||||
![alt cloud189](./src/view.png "使用图片")
|
||||
<h3 align="center">- cloud189-cli -</h3>
|
||||
|
||||
### 一:准备
|
||||
1. Python 版本 >= 3.6
|
||||
2. 安装依赖
|
||||
~~~shell
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
~~~
|
||||
```
|
||||
3. 配置
|
||||
打开``189.py``, 找到以下两行
|
||||
~~~python
|
||||
username = ""
|
||||
password = ""
|
||||
~~~
|
||||
修改为自己的天翼云盘手机号和密码即可
|
||||
运行 ``python cloud189-cli.py``, 输入用户名与密码
|
||||
|
||||
账号为自己的天翼云盘手机号,密码不会有回显。
|
||||
|
||||
也可以 自接两次回车后,输入 `clogin` 按提示输入 `cookie` 登录。
|
||||
|
||||
### 二:使用
|
||||
0. 不加参数者进入交互模式
|
||||
```sh
|
||||
python cloud189-cli.py # 提示符为 >
|
||||
|
||||
> bye# 退出
|
||||
```
|
||||
1. 查看**根目录**的文件
|
||||
~~~shell
|
||||
python 189.py list
|
||||
~~~
|
||||
```sh
|
||||
python cloud189-cli.py ls
|
||||
```
|
||||
2. 上传文件至**根目录**
|
||||
~~~shell
|
||||
python 189.py upload 文件
|
||||
~~~
|
||||
```sh
|
||||
python cloud189-cli.py upload '文件路径'
|
||||
```
|
||||
3. 下载**根目录**的文件
|
||||
~~~shell
|
||||
python 189.py download 文件ID # 文件ID 第一步看
|
||||
~~~
|
||||
```sh
|
||||
python cloud189-cli.py down 文件ID # 文件ID 第一步看
|
||||
```
|
||||
4. 分享**根目录**的文件
|
||||
~~~shell
|
||||
python 189.py share 文件ID # 文件ID 第一步看
|
||||
~~~
|
||||
```sh
|
||||
python cloud189-cli.py share 文件ID # 文件ID 第一步看
|
||||
```
|
||||
5. 删除**根目录**的文件
|
||||
~~~shell
|
||||
python 189.py delete 文件ID # 文件ID 第一步看
|
||||
~~~
|
||||
```sh
|
||||
python cloud189-cli.py delete 文件ID # 文件ID 第一步看
|
||||
```
|
||||
|
||||
### 三:免责
|
||||
您使用本工具做的任何事情都雨我无瓜。
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from cloud189.cli.cli import Commander
|
||||
from cloud189.cli.utils import set_console_style, print_logo, check_update, error
|
||||
|
||||
if __name__ == '__main__':
|
||||
set_console_style()
|
||||
# check_update()
|
||||
commander = Commander()
|
||||
commander.login()
|
||||
|
||||
if len(sys.argv) >= 2:
|
||||
cmd, arg = (sys.argv[1], '') if len(sys.argv) == 2 else (sys.argv[1], sys.argv[2])
|
||||
commander.run_one(cmd, arg)
|
||||
else:
|
||||
print_logo()
|
||||
while True:
|
||||
try:
|
||||
commander.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
error(e)
|
|
@ -0,0 +1 @@
|
|||
__all__ = ['api', 'cli']
|
|
@ -0,0 +1,5 @@
|
|||
from cloud189.api.core import Cloud189
|
||||
|
||||
version = '0.0.1'
|
||||
|
||||
__all__ = ['utils', 'Cloud189', 'version']
|
|
@ -0,0 +1,263 @@
|
|||
"""
|
||||
天翼云盘 API,封装了对天翼云的各种操作,解除了上传格式、大小限制
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
import requests
|
||||
from requests_toolbelt import MultipartEncoder, MultipartEncoderMonitor
|
||||
from urllib3 import disable_warnings
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from cloud189.api.utils import *
|
||||
|
||||
__all__ = ['Cloud189']
|
||||
|
||||
|
||||
class Cloud189(object):
|
||||
FAILED = -1
|
||||
SUCCESS = 0
|
||||
ID_ERROR = 1
|
||||
PASSWORD_ERROR = 2
|
||||
LACK_PASSWORD = 3
|
||||
ZIP_ERROR = 4
|
||||
MKDIR_ERROR = 5
|
||||
URL_INVALID = 6
|
||||
FILE_CANCELLED = 7
|
||||
PATH_ERROR = 8
|
||||
NETWORK_ERROR = 9
|
||||
CAPTCHA_ERROR = 10
|
||||
|
||||
def __init__(self):
|
||||
self._session = requests.Session()
|
||||
self._captcha_handler = None
|
||||
self._timeout = 15 # 每个请求的超时(不包含下载响应体的用时)
|
||||
self._host_url = 'https://cloud.189.cn'
|
||||
self._auth_url = 'https://open.e.189.cn/api/logbox/oauth2/'
|
||||
self._cookies = None
|
||||
self._headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0',
|
||||
'Referer': 'https://open.e.189.cn/',
|
||||
}
|
||||
disable_warnings(InsecureRequestWarning) # 全局禁用 SSL 警告
|
||||
|
||||
def _get(self, url, **kwargs):
|
||||
try:
|
||||
kwargs.setdefault('timeout', self._timeout)
|
||||
kwargs.setdefault('headers', self._headers)
|
||||
return self._session.get(url, verify=False, **kwargs)
|
||||
except requests.Timeout:
|
||||
logger.warning("Encountered timeout error while requesting network!")
|
||||
raise TimeoutError
|
||||
except (requests.RequestException, Exception) as e:
|
||||
logger.error(f"Unexpected error: {e=}")
|
||||
|
||||
def _post(self, url, data, **kwargs):
|
||||
try:
|
||||
kwargs.setdefault('timeout', self._timeout)
|
||||
kwargs.setdefault('headers', self._headers)
|
||||
return self._session.post(url, data, verify=False, **kwargs)
|
||||
except requests.Timeout:
|
||||
logger.warning("Encountered timeout error while requesting network!")
|
||||
raise TimeoutError
|
||||
except (requests.RequestException, Exception) as e:
|
||||
logger.error(f"Unexpected error: {e=}")
|
||||
|
||||
def get_cookie(self):
|
||||
return self._session.cookies.get_dict()
|
||||
|
||||
def login_by_cookie(self, cookies: dict):
|
||||
try:
|
||||
for k, v in cookies.items():
|
||||
self._session.cookies.set(k, v, domain=".cloud.189.cn")
|
||||
|
||||
r = self._get(self._host_url + "/v2/getUserLevelInfo.action")
|
||||
if "InvalidSessionKey" not in r.text:
|
||||
return Cloud189.SUCCESS
|
||||
except: pass
|
||||
return Cloud189.FAILED
|
||||
|
||||
def login(self, username, password):
|
||||
captchaToken, returnUrl, paramId = self.redirect()
|
||||
validateCode = self.needcaptcha(captchaToken, username)
|
||||
url = self._auth_url + "loginSubmit.do"
|
||||
data = {
|
||||
"appKey": "cloud",
|
||||
"accountType": '01',
|
||||
"userName": "{RSA}" + b64tohex(encrypt(username)),
|
||||
"password": "{RSA}" + b64tohex(encrypt(password)),
|
||||
"validateCode": validateCode,
|
||||
"captchaToken": captchaToken,
|
||||
"returnUrl": returnUrl,
|
||||
"mailSuffix": "@189.cn",
|
||||
"paramId": paramId
|
||||
}
|
||||
r = self._post(url, data=data)
|
||||
msg = r.json()["msg"]
|
||||
if msg == "登录成功":
|
||||
self._get(r.json()["toUrl"])
|
||||
return Cloud189.SUCCESS
|
||||
print(msg)
|
||||
return Cloud189.FAILED
|
||||
|
||||
def redirect(self):
|
||||
r = self._get(self._host_url + "/udb/udb_login.jsp?pageId=1&redirectURL=/main.action")
|
||||
captchaToken = re.findall(r"captchaToken' value='(.+?)'", r.text)[0]
|
||||
lt = re.findall(r'lt = "(.+?)"', r.text)[0]
|
||||
returnUrl = re.findall(r"returnUrl = '(.+?)'", r.text)[0]
|
||||
paramId = re.findall(r'paramId = "(.+?)"', r.text)[0]
|
||||
self._session.headers.update({"lt": lt})
|
||||
return captchaToken, returnUrl, paramId
|
||||
|
||||
def needcaptcha(self, captchaToken, username):
|
||||
r = self._post(
|
||||
url= self._auth_url + "needcaptcha.do",
|
||||
data={
|
||||
"accountType": "01",
|
||||
"userName": "{RSA}" + b64tohex(encrypt(username)),
|
||||
"appKey": "cloud"
|
||||
}
|
||||
)
|
||||
if r.text == "0":
|
||||
print("DONT NEED CAPTCHA")
|
||||
return ""
|
||||
else:
|
||||
print("NEED CAPTCHA")
|
||||
r = self._get(
|
||||
url= self._auth_url + "picCaptcha.do",
|
||||
params={"token": captchaToken}
|
||||
)
|
||||
with open("./captcha.png", "wb") as f:
|
||||
f.write(r.content)
|
||||
f.close()
|
||||
return input("验证码下载完成,打开 ./captcha.png 查看: ")
|
||||
|
||||
def get_file_size_str(self, filesize: int) -> str:
|
||||
if 0 < filesize < 1024**2:
|
||||
return f"{round(filesize/1024, 2)}KB"
|
||||
elif 1024**2 < filesize < 1024**3:
|
||||
return f"{round(filesize/1024**2, 2)}MB"
|
||||
elif 1024**3 < filesize < 1024**4:
|
||||
return f"{round(filesize/1024**3, 2)}GB"
|
||||
elif 1024**4 < filesize < 1024**5:
|
||||
return f"{round(filesize/1024**4, 2)}TB"
|
||||
else: return f"{filesize}Bytes"
|
||||
|
||||
def share_file(self, fid):
|
||||
expireTime_dict = {
|
||||
"1": "1",
|
||||
"2": "7",
|
||||
"3": "2099"
|
||||
}
|
||||
expireTime = input("请选择分享有效期:1、1天,2、7天,3、永久:")
|
||||
withAccessCode = input("请选择分享形式:1、私密分享,2、公开分享:")
|
||||
if withAccessCode == "1":
|
||||
url = self._host_url + "/v2/privateLinkShare.action"
|
||||
params = {
|
||||
"fileId": fid,
|
||||
"expireTime": expireTime_dict[expireTime],
|
||||
"withAccessCode": withAccessCode
|
||||
}
|
||||
else:
|
||||
url = self._host_url + "/v2/createOutLinkShare.action"
|
||||
params = {
|
||||
"fileId": fid,
|
||||
"expireTime": expireTime_dict[expireTime]
|
||||
}
|
||||
r = self._get(url=url, params=params).json()
|
||||
msg = f"链接:{r['shortShareUrl']} "
|
||||
msg += "" if not r.get("accessCode") else f"访问码:{r['accessCode']}"
|
||||
print(msg)
|
||||
|
||||
def get_files(self):
|
||||
url = self._host_url + "/v2/listFiles.action"
|
||||
params = {
|
||||
"fileId": "-11", # 根目录
|
||||
"inGroupSpace": "false",
|
||||
"orderBy": "1",
|
||||
"order": "ASC",
|
||||
"pageNum": "1",
|
||||
"pageSize": "60"
|
||||
}
|
||||
r = self._get(url, params=params).json()
|
||||
for file in r["data"]:
|
||||
folder_or_file = "文件夹: " if file["isFolder"] else f"大小: {self.get_file_size_str(file['fileSize'])} 文件名: "
|
||||
filename = file["fileName"]
|
||||
print(f"{folder_or_file}{filename} {'' if file['isFolder'] else '文件ID: ' + file['fileId']}")
|
||||
|
||||
def upload(self, filePath):
|
||||
self._session.headers["Referer"] = self._host_url
|
||||
def get_upload_url():
|
||||
r = self._get(self._host_url + "/v2/getUserUploadUrl.action")
|
||||
# print(r.text)
|
||||
return "https:" + r.json()["uploadUrl"]
|
||||
|
||||
def get_session_key():
|
||||
r = self._get(
|
||||
url=self._host_url + "/main.action",
|
||||
headers={"Host": "cloud.189.cn"}
|
||||
)
|
||||
sessionKey = re.findall(r"sessionKey = '(.+?)'", r.text)[0]
|
||||
return sessionKey
|
||||
|
||||
def upload_file():
|
||||
filename = os.path.basename(filePath)
|
||||
filesize = os.path.getsize(filePath)
|
||||
print(f"正在上传: {filename} 大小: {self.get_file_size_str(filesize)}")
|
||||
upload_url = get_upload_url()
|
||||
multipart_data = MultipartEncoder(
|
||||
fields={
|
||||
"sessionKey": get_session_key(),
|
||||
"parentId": "-11", # 上传文件夹 根目录
|
||||
"albumId": "undefined",
|
||||
"opertype": "1",
|
||||
"fname": filename,
|
||||
'file': (filename, open(filePath, 'rb'), 'application/octet-stream')
|
||||
}
|
||||
)
|
||||
headers = {"Content-Type": multipart_data.content_type}
|
||||
r = self._post(url=upload_url, data=multipart_data, headers=headers).json()
|
||||
print(f"上传完毕!文件ID:{r['id']} 上传时间: {r['createDate']}")
|
||||
upload_file()
|
||||
|
||||
def get_file_info_by_id(self, fid):
|
||||
infos = {}
|
||||
r = self._get(self._host_url + f"/v2/getFileInfo.action?fileId={fid}").json()
|
||||
# print(r)
|
||||
infos["fname"] = r["fileName"]
|
||||
infos["fid"] = fid
|
||||
infos["size"] = r['fileSize']
|
||||
infos["srcParentId"] = "-11" # 根目录
|
||||
infos["isfolder"] = True if r["isFolder"] else False
|
||||
infos["durl"] = "https:"+r["downloadUrl"]
|
||||
return infos
|
||||
|
||||
def download_by_id(self, fid, save_path='./downloads'):
|
||||
infos = self.get_file_info_by_id(fid)
|
||||
if infos:
|
||||
if not os.path.isdir(save_path):
|
||||
os.mkdir(save_path)
|
||||
r = self._get(infos['durl'], stream=True)
|
||||
save_path = save_path + os.sep + infos['fname']
|
||||
with open(save_path, "wb") as f:
|
||||
for chunk in r.iter_content(chunk_size=1024**2):
|
||||
f.write(chunk)
|
||||
f.close()
|
||||
print(f"{infos['fname']} 下载完成!")
|
||||
|
||||
def delete_by_id(self, fid):
|
||||
infos = self.get_file_info_by_id(fid)
|
||||
|
||||
taskInfo = {"fileId": infos["fid"],
|
||||
"srcParentId": "-11", # 根目录
|
||||
"fileName": infos["fname"],
|
||||
"isFolder": 1 if infos["isfolder"] else 0 }
|
||||
|
||||
url = self._host_url + "/createBatchTask.action"
|
||||
post_data = { "type": "DELETE", "taskInfos": json.dumps([taskInfo,])}
|
||||
|
||||
r = self._post(url, data=post_data)
|
||||
if r.text:
|
||||
print("删除成功!")
|
|
@ -0,0 +1,77 @@
|
|||
"""
|
||||
API 处理网页数据、数据切片时使用的工具
|
||||
"""
|
||||
|
||||
import logging
|
||||
import hashlib
|
||||
import rsa
|
||||
from base64 import b64encode
|
||||
|
||||
__all__ = ['logger', 'md5', 'encrypt', 'int2char', 'b64tohex']
|
||||
|
||||
# 调试日志设置
|
||||
logger = logging.getLogger('lanzou')
|
||||
logger.setLevel(logging.ERROR)
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s [line:%(lineno)d] %(funcName)s %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S")
|
||||
console = logging.StreamHandler()
|
||||
console.setFormatter(formatter)
|
||||
logger.addHandler(console)
|
||||
|
||||
|
||||
RSA_KEY = """-----BEGIN PUBLIC KEY-----
|
||||
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDY7mpaUysvgQkbp0iIn2ezoUyh
|
||||
i1zPFn0HCXloLFWT7uoNkqtrphpQ/63LEcPz1VYzmDuDIf3iGxQKzeoHTiVMSmW6
|
||||
FlhDeqVOG094hFJvZeK4OzA6HVwzwnEW5vIZ7d+u61RV1bsFxmB68+8JXs3ycGcE
|
||||
4anY+YzZJcyOcEGKVQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
|
||||
def encrypt(password: str) -> str:
|
||||
return b64encode(
|
||||
rsa.encrypt(
|
||||
(password).encode('utf-8'),
|
||||
rsa.PublicKey.load_pkcs1_openssl_pem(RSA_KEY.encode())
|
||||
)
|
||||
).decode()
|
||||
|
||||
b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
BI_RM = list("0123456789abcdefghijklmnopqrstuvwxyz")
|
||||
|
||||
def int2char(a):
|
||||
return BI_RM[a]
|
||||
|
||||
def b64tohex(a):
|
||||
d = ""
|
||||
e = 0
|
||||
for i in range(len(a)):
|
||||
if list(a)[i] != "=":
|
||||
v = b64map.index(list(a)[i])
|
||||
if 0 == e:
|
||||
e = 1
|
||||
d += int2char(v >> 2)
|
||||
c = 3 & v
|
||||
elif 1 == e:
|
||||
e = 2
|
||||
d += int2char(c << 2 | v >> 4)
|
||||
c = 15 & v
|
||||
elif 2 == e:
|
||||
e = 3
|
||||
d += int2char(c)
|
||||
d += int2char(v >> 2)
|
||||
c = 3 & v
|
||||
else:
|
||||
e = 0
|
||||
d += int2char(c << 2 | v >> 4)
|
||||
d += int2char(15 & v)
|
||||
if e == 1:
|
||||
d += int2char(c << 2)
|
||||
# print(d)
|
||||
return d
|
||||
|
||||
|
||||
def md5(s):
|
||||
hl = hashlib.md5()
|
||||
hl.update(s.encode(encoding='utf-8'))
|
||||
return hl.hexdigest()
|
|
@ -0,0 +1,5 @@
|
|||
from cloud189.cli.config import config
|
||||
|
||||
version = '0.0.1'
|
||||
|
||||
__all__ = ['cli', 'utils', 'version', 'config']
|
|
@ -0,0 +1,493 @@
|
|||
from getpass import getpass
|
||||
from sys import exit as exit_cmd
|
||||
from webbrowser import open_new_tab
|
||||
|
||||
from cloud189.api import Cloud189
|
||||
|
||||
from cloud189.cli import config
|
||||
# from cloud189.cli.downloader import Downloader, Uploader
|
||||
# from cloud189.cli.recovery import Recovery
|
||||
# from cloud189.cli.manager import global_task_mgr
|
||||
from cloud189.cli.utils import *
|
||||
|
||||
|
||||
class Commander:
|
||||
"""网盘命令行"""
|
||||
|
||||
def __init__(self):
|
||||
self._prompt = '> '
|
||||
self._disk = Cloud189()
|
||||
# self._task_mgr = global_task_mgr
|
||||
self._dir_list = ''
|
||||
self._file_list = ''
|
||||
self._path_list = ''
|
||||
self._parent_id = -1
|
||||
self._parent_name = ''
|
||||
self._work_name = ''
|
||||
self._work_id = -1
|
||||
self._last_work_id = -1
|
||||
self._reader_mode = False
|
||||
self._reader_mode = config.reader_mode
|
||||
self._default_dir_pwd = ''
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
clear_screen()
|
||||
|
||||
@staticmethod
|
||||
def help():
|
||||
print_help()
|
||||
|
||||
@staticmethod
|
||||
def update():
|
||||
check_update()
|
||||
|
||||
def bye(self):
|
||||
# if self._task_mgr.has_alive_task():
|
||||
# info(f"有任务在后台运行, 退出请直接关闭窗口")
|
||||
# else:
|
||||
exit_cmd(0)
|
||||
|
||||
def rmode(self):
|
||||
"""适用于屏幕阅读器用户的显示方式"""
|
||||
choice = input("以适宜屏幕阅读器的方式显示(y): ")
|
||||
if choice and choice.lower() == 'y':
|
||||
config.reader_mode = True
|
||||
self._reader_mode = True
|
||||
info("已启用 Reader Mode")
|
||||
else:
|
||||
config.reader_mode = False
|
||||
self._reader_mode = False
|
||||
info("已关闭 Reader Mode")
|
||||
|
||||
def cdrec(self):
|
||||
"""进入回收站模式"""
|
||||
pass
|
||||
# rec = Recovery(self._disk)
|
||||
# rec.run()
|
||||
# self.refresh()
|
||||
|
||||
def xghost(self):
|
||||
"""扫描并删除幽灵文件夹"""
|
||||
choice = input("需要清理幽灵文件夹吗(y): ")
|
||||
if choice and choice.lower() == 'y':
|
||||
self._disk.clean_ghost_folders()
|
||||
info("清理已完成")
|
||||
else:
|
||||
info("清理操作已取消")
|
||||
|
||||
def refresh(self, dir_id=None):
|
||||
"""刷新当前文件夹和路径信息"""
|
||||
pass
|
||||
'''
|
||||
dir_id = self._work_id if dir_id is None else dir_id
|
||||
self._file_list = self._disk.get_file_list(dir_id)
|
||||
self._dir_list = self._disk.get_dir_list(dir_id)
|
||||
self._path_list = self._disk.get_full_path(dir_id)
|
||||
self._prompt = '/'.join(self._path_list.all_name) + ' > '
|
||||
self._last_work_id = self._work_id
|
||||
self._work_name = self._path_list[-1].name
|
||||
self._work_id = self._path_list[-1].id
|
||||
if dir_id != -1: # 如果存在上级路径
|
||||
self._parent_name = self._path_list[-2].name
|
||||
self._parent_id = self._path_list[-2].id'''
|
||||
|
||||
def login(self):
|
||||
"""登录网盘"""
|
||||
if not config.cookie or self._disk.login_by_cookie(config.cookie) != Cloud189.SUCCESS:
|
||||
username = input('输入用户名:')
|
||||
password = getpass('输入密码:')
|
||||
code = self._disk.login(username, password)
|
||||
if code == Cloud189.NETWORK_ERROR:
|
||||
error("登录失败,网络连接异常")
|
||||
return None
|
||||
elif code == Cloud189.FAILED:
|
||||
error('登录失败,用户名或密码错误 :(')
|
||||
return None
|
||||
# 登录成功保存用户 cookie
|
||||
config.cookie = self._disk.get_cookie()
|
||||
self.refresh()
|
||||
|
||||
|
||||
def clogin(self):
|
||||
"""使用 cookie 登录"""
|
||||
open_new_tab('https://cloud.189.cn')
|
||||
info("请设置 Cookie 内容:")
|
||||
c_login_user = input("COOKIE_LOGIN_USER=")
|
||||
if not c_login_user:
|
||||
error("请输入正确的 Cookie 信息")
|
||||
return None
|
||||
cookie = {"COOKIE_LOGIN_USER": str(c_login_user)}
|
||||
if self._disk.login_by_cookie(cookie) == Cloud189.SUCCESS:
|
||||
config.cookie = cookie
|
||||
self.refresh()
|
||||
else:
|
||||
error("登录失败,请检查 Cookie 是否正确")
|
||||
|
||||
def logout(self):
|
||||
"""注销"""
|
||||
clear_screen()
|
||||
self._prompt = '> '
|
||||
self._disk.logout()
|
||||
self._file_list.clear()
|
||||
self._dir_list.clear()
|
||||
self._path_list = ''
|
||||
self._parent_id = -1
|
||||
self._work_id = -1
|
||||
self._last_work_id = -1
|
||||
self._parent_name = ''
|
||||
self._work_name = ''
|
||||
|
||||
config.cookie = None
|
||||
|
||||
def ls(self):
|
||||
"""列出文件(夹)"""
|
||||
self._disk.get_files()
|
||||
'''
|
||||
if self._reader_mode: # 方便屏幕阅读器阅读
|
||||
for folder in self._dir_list:
|
||||
pwd_str = '有提取码' if folder.has_pwd else ''
|
||||
print(f"{folder.name}/ {folder.desc} {pwd_str}")
|
||||
for file in self._file_list:
|
||||
pwd_str = '有提取码' if file.has_pwd else ''
|
||||
print(f"{file.name} 大小:{file.size} 上传时间:{file.time} 下载次数:{file.downs} {pwd_str}")
|
||||
else: # 普通用户显示方式
|
||||
for folder in self._dir_list:
|
||||
pwd_str = '✦' if folder.has_pwd else '✧'
|
||||
print("#{0:<12}{1:<4}{2}{3}/".format(
|
||||
folder.id, pwd_str, text_align(folder.desc, 28), folder.name))
|
||||
for file in self._file_list:
|
||||
pwd_str = '✦' if file.has_pwd else '✧'
|
||||
print("#{0:<12}{1:<4}{2:<12}{3:>8}{4:>6} {5}".format(
|
||||
file.id, pwd_str, file.time, file.size, file.downs, file.name))
|
||||
'''
|
||||
|
||||
def cd(self, dir_name):
|
||||
"""切换工作目录"""
|
||||
if not dir_name:
|
||||
info('cd .. 返回上级路径, cd - 返回上次路径, cd / 返回根目录')
|
||||
elif dir_name == '..':
|
||||
self.refresh(self._parent_id)
|
||||
elif dir_name == '/':
|
||||
self.refresh(-1)
|
||||
elif dir_name == '-':
|
||||
self.refresh(self._last_work_id)
|
||||
elif dir_name == '.':
|
||||
pass
|
||||
elif folder := self._dir_list.find_by_name(dir_name):
|
||||
self.refresh(folder.id)
|
||||
else:
|
||||
error(f'文件夹不存在: {dir_name}')
|
||||
|
||||
def mkdir(self, name):
|
||||
"""创建文件夹"""
|
||||
if self._dir_list.find_by_name(name):
|
||||
error(f'文件夹已存在: {name}')
|
||||
return None
|
||||
|
||||
dir_id = self._disk.mkdir(self._work_id, name, '')
|
||||
if dir_id == Cloud189.MKDIR_ERROR:
|
||||
error(f'创建文件夹失败(深度最大 4 级)')
|
||||
return None
|
||||
# 创建成功,添加到文件夹列表,减少向服务器请求次数
|
||||
self._disk.set_passwd(dir_id, self._default_dir_pwd, is_file=False)
|
||||
# self._dir_list.append(Folder(name, dir_id, bool(self._default_dir_pwd), ''))
|
||||
|
||||
def rm(self, fid):
|
||||
"""删除文件(夹)"""
|
||||
if fid:
|
||||
self._disk.delete_by_id(fid)
|
||||
# if file := self._file_list.find_by_name(name): # 删除文件
|
||||
# if self._disk.delete(file.id, True) == Cloud189.SUCCESS:
|
||||
# self._file_list.pop_by_id(file.id)
|
||||
# else:
|
||||
# error(f'删除文件失败: {name}')
|
||||
# elif folder := self._dir_list.find_by_name(name): # 删除文件夹
|
||||
# if self._disk.delete(folder.id, False) == Cloud189.SUCCESS:
|
||||
# self._dir_list.pop_by_id(folder.id)
|
||||
# else:
|
||||
# error(f'删除文件夹失败(存在子文件夹?): {name}')
|
||||
# else:
|
||||
# error(f'文件(夹)不存在: {name}')
|
||||
|
||||
def rename(self, name):
|
||||
"""重命名文件或文件夹(需要会员)"""
|
||||
if folder := self._dir_list.find_by_name(name):
|
||||
fid, is_file = folder.id, False
|
||||
elif file := self._file_list.find_by_name(name):
|
||||
fid, is_file = file.id, True
|
||||
else:
|
||||
error(f'没有这个文件(夹)的啦: {name}')
|
||||
return None
|
||||
|
||||
new_name = input(f'重命名 "{name}" 为 ') or ''
|
||||
if not new_name:
|
||||
info(f'重命名操作取消')
|
||||
return None
|
||||
|
||||
if is_file:
|
||||
if self._disk.rename_file(fid, new_name) != Cloud189.SUCCESS:
|
||||
error('(#°Д°) 文件重命名失败, 请开通会员,文件名不要带后缀')
|
||||
return None
|
||||
# 只更新本地索引的文件夹名(调用refresh()要等 1.5s 才能刷新信息)
|
||||
self._file_list.update_by_id(fid, name=name)
|
||||
else:
|
||||
if self._disk.rename_dir(fid, new_name) != Cloud189.SUCCESS:
|
||||
error('文件夹重命名失败')
|
||||
return None
|
||||
self._dir_list.update_by_id(fid, name=new_name)
|
||||
|
||||
def mv(self, name):
|
||||
"""移动文件或文件夹"""
|
||||
if file := self._file_list.find_by_name(name):
|
||||
fid, is_file = file.id, True
|
||||
elif folder := self._dir_list.find_by_name(name):
|
||||
fid, is_file = folder.id, False
|
||||
else:
|
||||
error(f"文件(夹)不存在: {name}")
|
||||
return None
|
||||
|
||||
path_list = self._disk.get_move_paths()
|
||||
path_list = {'/'.join(path.all_name): path[-1].id for path in path_list}
|
||||
choice_list = list(path_list.keys())
|
||||
|
||||
def _condition(typed_str, choice_str):
|
||||
path_depth = len(choice_str.split('/'))
|
||||
# 没有输入时, 补全 Cloud189,深度 1
|
||||
if not typed_str and path_depth == 1:
|
||||
return True
|
||||
# Cloud189/ 深度为 2,补全同深度的文件夹 Cloud189/test 、Cloud189/txt
|
||||
# Cloud189/tx 应该补全 Cloud189/txt
|
||||
if path_depth == len(typed_str.split('/')) and choice_str.startswith(typed_str):
|
||||
return True
|
||||
|
||||
set_completer(choice_list, condition=_condition)
|
||||
choice = input('请输入路径(TAB键补全) : ')
|
||||
if not choice or choice not in choice_list:
|
||||
error(f"目标路径不存在: {choice}")
|
||||
return None
|
||||
folder_id = path_list.get(choice)
|
||||
if is_file:
|
||||
if self._disk.move_file(fid, folder_id) == Cloud189.SUCCESS:
|
||||
self._file_list.pop_by_id(fid)
|
||||
else:
|
||||
error(f"移动文件到 {choice} 失败")
|
||||
else:
|
||||
if self._disk.move_folder(fid, folder_id) == Cloud189.SUCCESS:
|
||||
self._dir_list.pop_by_id(fid)
|
||||
else:
|
||||
error(f"移动文件夹到 {choice} 失败")
|
||||
|
||||
def down(self, arg):
|
||||
"""自动选择下载方式"""
|
||||
if arg:
|
||||
self._disk.download_by_id(arg)
|
||||
# downloader = Downloader(self._disk)
|
||||
# if arg.startswith('http'):
|
||||
# downloader.set_url(arg)
|
||||
# elif file := self._file_list.find_by_name(arg): # 如果是文件
|
||||
# path = '/'.join(self._path_list.all_name) + '/' + arg # 文件在网盘的绝对路径
|
||||
# downloader.set_fid(file.id, is_file=True, f_path=path)
|
||||
# elif folder := self._dir_list.find_by_name(arg): # 如果是文件夹
|
||||
# path = '/'.join(self._path_list.all_name) + '/' + arg + '/' # 文件夹绝对路径, 加 '/' 以便区分
|
||||
# downloader.set_fid(folder.id, is_file=False, f_path=path)
|
||||
# else:
|
||||
# error(f'文件(夹)不存在: {arg}')
|
||||
# return None
|
||||
# # 提交下载任务
|
||||
# self._task_mgr.add_task(downloader)
|
||||
|
||||
def jobs(self, arg):
|
||||
"""显示后台任务列表"""
|
||||
pass
|
||||
# if arg.isnumeric():
|
||||
# self._task_mgr.show_detail(int(arg))
|
||||
# else:
|
||||
# self._task_mgr.show_tasks()
|
||||
|
||||
def upload(self, path):
|
||||
"""上传文件(夹)"""
|
||||
path = path.strip('\"\' ') # 去除直接拖文件到窗口产生的引号
|
||||
if not os.path.exists(path):
|
||||
error(f'该路径不存在哦: {path}')
|
||||
return None
|
||||
self._disk.upload(path)
|
||||
# uploader = Uploader(self._disk)
|
||||
# if os.path.isfile(path):
|
||||
# uploader.set_upload_path(path, is_file=True)
|
||||
# else:
|
||||
# uploader.set_upload_path(path, is_file=False)
|
||||
# uploader.set_target(self._work_id, self._work_name)
|
||||
# self._task_mgr.add_task(uploader)
|
||||
|
||||
def share(self, fid):
|
||||
"""分享文件"""
|
||||
if fid:
|
||||
self._disk.share_file(fid)
|
||||
# if file := self._file_list.find_by_name(name): # 文件
|
||||
# inf = self._disk.get_file_info_by_id(file.id)
|
||||
# if inf.code != Cloud189.SUCCESS:
|
||||
# error('获取文件信息出错')
|
||||
# return None
|
||||
|
||||
# print("-" * 50)
|
||||
# print(f"文件名 : {name}")
|
||||
# print(f"提取码 : {inf.pwd or '无'}")
|
||||
# print(f"文件大小 : {inf.size}")
|
||||
# print(f"上传时间 : {inf.time}")
|
||||
# print(f"分享链接 : {inf.url}")
|
||||
# print(f"描述信息 : {inf.desc or '无'}")
|
||||
# print(f"下载直链 : {inf.durl or '无'}")
|
||||
# print("-" * 50)
|
||||
|
||||
# elif folder := self._dir_list.find_by_name(name): # 文件夹
|
||||
# inf = self._disk.get_folder_info_by_id(folder.id)
|
||||
# if inf.code != Cloud189.SUCCESS:
|
||||
# print('ERROR : 获取文件夹信息出错')
|
||||
# return None
|
||||
|
||||
# print("-" * 80)
|
||||
# print(f"文件夹名 : {name}")
|
||||
# print(f"提取码 : {inf.folder.pwd or '无'}")
|
||||
# print(f"分享链接 : {inf.folder.url}")
|
||||
# print(f"描述信息 : {inf.folder.desc or '无'}")
|
||||
# print("-" * 80)
|
||||
|
||||
# for file in inf.files:
|
||||
# print("+ {0:<12}{1:<9}{2}\t{3}".format(file.time, file.size, file.url, file.name))
|
||||
# if len(inf.files) != 0:
|
||||
# print("-" * 80)
|
||||
# else:
|
||||
# error(f"文件(夹)不存在: {name}")
|
||||
|
||||
def passwd(self, name):
|
||||
"""设置文件(夹)提取码"""
|
||||
if file := self._file_list.find_by_name(name): # 文件
|
||||
inf = self._disk.get_share_info(file.id, True)
|
||||
new_pass = input(f'修改提取码 "{inf.pwd or "无"}" -> ')
|
||||
if 2 <= len(new_pass) <= 6:
|
||||
if new_pass == 'off': new_pass = ''
|
||||
if self._disk.set_passwd(file.id, str(new_pass), True) != Cloud189.SUCCESS:
|
||||
error('设置文件提取码失败')
|
||||
self.refresh()
|
||||
else:
|
||||
error('提取码为2-6位字符,关闭请输入off')
|
||||
elif folder := self._dir_list.find_by_name(name): # 文件夹
|
||||
inf = self._disk.get_share_info(folder.id, False)
|
||||
new_pass = input(f'修改提取码 "{inf.pwd or "无"}" -> ')
|
||||
if 2 <= len(new_pass) <= 12:
|
||||
if new_pass == 'off': new_pass = ''
|
||||
if self._disk.set_passwd(folder.id, str(new_pass), False) != Cloud189.SUCCESS:
|
||||
error('设置文件夹提取码失败')
|
||||
self.refresh()
|
||||
else:
|
||||
error('提取码为2-12位字符,关闭请输入off')
|
||||
else:
|
||||
error(f'文件(夹)不存在: {name}')
|
||||
|
||||
def desc(self, name):
|
||||
"""设置文件描述"""
|
||||
if file := self._file_list.find_by_name(name): # 文件
|
||||
inf = self._disk.get_share_info(file.id, True)
|
||||
print(f"当前描述: {inf.desc or '无'}")
|
||||
desc = input(f'修改为 -> ')
|
||||
if not desc:
|
||||
error(f'文件描述不允许为空')
|
||||
return None
|
||||
if self._disk.set_desc(file.id, str(desc), True) != Cloud189.SUCCESS:
|
||||
error(f'文件描述修改失败')
|
||||
self.refresh()
|
||||
elif folder := self._dir_list.find_by_name(name): # 文件夹
|
||||
inf = self._disk.get_share_info(folder.id, False)
|
||||
print(f"当前描述: {inf.desc}")
|
||||
desc = input(f'修改为 -> ') or ''
|
||||
if self._disk.set_desc(folder.id, str(desc), False) == Cloud189.SUCCESS:
|
||||
if len(desc) == 0:
|
||||
info('文件夹描述已关闭')
|
||||
else:
|
||||
error(f'文件夹描述修改失败')
|
||||
self.refresh()
|
||||
else:
|
||||
error(f'文件(夹)不存在: {name}')
|
||||
|
||||
def setpath(self):
|
||||
"""设置下载路径"""
|
||||
print(f"当前下载路径 : {config.save_path}")
|
||||
path = input('修改为 -> ').strip("\"\' ")
|
||||
if os.path.isdir(path):
|
||||
config.save_path = path
|
||||
else:
|
||||
error('路径非法,取消修改')
|
||||
|
||||
def setsize(self):
|
||||
"""设置上传限制"""
|
||||
print(f"当前限制(MB): {config.max_size}")
|
||||
max_size = input('修改为 -> ')
|
||||
if not max_size.isnumeric():
|
||||
error("请输入大于 100 的数字")
|
||||
return None
|
||||
if self._disk.set_max_size(int(max_size)) != Cloud189.SUCCESS:
|
||||
error("设置失败,限制值必需大于 100")
|
||||
return None
|
||||
config.max_size = int(max_size)
|
||||
|
||||
def setdelay(self):
|
||||
"""设置大文件上传延时"""
|
||||
print("大文件数据块上传延时范围(秒), 如: 0 60")
|
||||
print(f"当前配置: {config.upload_delay}")
|
||||
tr = input("请输入延时范围: ").split()
|
||||
if len(tr) != 2:
|
||||
error("格式有误!")
|
||||
return None
|
||||
tr = (int(tr[0]), int(tr[1]))
|
||||
self._disk.set_upload_delay(tr)
|
||||
config.upload_delay = tr
|
||||
|
||||
def setpasswd(self):
|
||||
"""设置文件(夹)默认上传密码"""
|
||||
print("关闭提取码请输入 off")
|
||||
print(f"当前配置: 文件: {config.default_file_pwd or '无'}, 文件夹: {config.default_dir_pwd or '无'}")
|
||||
file_pwd = input("设置文件默认提取码(2-6位): ")
|
||||
if 2 <= len(file_pwd) <= 6:
|
||||
config.default_file_pwd = '' if file_pwd == 'off' else file_pwd
|
||||
dir_pwd = input("设置文件夹默认提取码(2-12位): ")
|
||||
if 2 <= len(dir_pwd) <= 12:
|
||||
config.default_dir_pwd = '' if dir_pwd == 'off' else dir_pwd
|
||||
info(f"修改成功: 文件: {config.default_file_pwd or '无'}, 文件夹: {config.default_dir_pwd or '无'}, 配置将在下次启动时生效")
|
||||
|
||||
def run_one(self, cmd, arg):
|
||||
no_arg_cmd = ['bye', 'cdrec', 'clear', 'clogin', 'help', 'login', 'logout', 'ls', 'refresh', 'rmode', 'setpath',
|
||||
'setsize', 'update', 'xghost', 'setdelay', 'setpasswd']
|
||||
cmd_with_arg = ['cd', 'desc', 'down', 'jobs', 'mkdir', 'mv', 'passwd', 'rename', 'rm', 'share', 'upload']
|
||||
|
||||
if cmd in no_arg_cmd:
|
||||
getattr(self, cmd)()
|
||||
elif cmd in cmd_with_arg:
|
||||
getattr(self, cmd)(arg)
|
||||
|
||||
def run(self):
|
||||
"""处理一条用户命令"""
|
||||
no_arg_cmd = ['bye', 'cdrec', 'clear', 'clogin', 'help', 'login', 'logout', 'ls', 'refresh', 'rmode', 'setpath',
|
||||
'setsize', 'update', 'xghost', 'setdelay', 'setpasswd']
|
||||
cmd_with_arg = ['cd', 'desc', 'down', 'jobs', 'mkdir', 'mv', 'passwd', 'rename', 'rm', 'share', 'upload']
|
||||
|
||||
choice_list = self._file_list + self._dir_list
|
||||
# choice_list = self._file_list.all_name + self._dir_list.all_name
|
||||
cmd_list = no_arg_cmd + cmd_with_arg
|
||||
set_completer(choice_list, cmd_list=cmd_list)
|
||||
|
||||
try:
|
||||
args = input(self._prompt).split(' ', 1)
|
||||
if len(args) == 0:
|
||||
return None
|
||||
except KeyboardInterrupt:
|
||||
print('')
|
||||
info('退出本程序请输入 bye')
|
||||
return None
|
||||
|
||||
cmd, arg = (args[0], '') if len(args) == 1 else (args[0], args[1]) # 命令, 参数(可带有空格, 没有参数就设为空)
|
||||
|
||||
if cmd in no_arg_cmd:
|
||||
getattr(self, cmd)()
|
||||
elif cmd in cmd_with_arg:
|
||||
getattr(self, cmd)(arg)
|
|
@ -0,0 +1,118 @@
|
|||
from pickle import load, dump
|
||||
|
||||
__all__ = ['config']
|
||||
KEY = 152
|
||||
config_file = './.config'
|
||||
|
||||
|
||||
def encrypt(key, s):
|
||||
b = bytearray(str(s).encode("utf-8"))
|
||||
n = len(b)
|
||||
c = bytearray(n*2)
|
||||
j = 0
|
||||
for i in range(0, n):
|
||||
b1 = b[i]
|
||||
b2 = b1 ^ key
|
||||
c1 = b2 % 19
|
||||
c2 = b2 // 19
|
||||
c1 = c1 + 46
|
||||
c2 = c2 + 46
|
||||
c[j] = c1
|
||||
c[j+1] = c2
|
||||
j = j+2
|
||||
return c.decode("utf-8")
|
||||
|
||||
|
||||
def decrypt(ksa, s):
|
||||
c = bytearray(str(s).encode("utf-8"))
|
||||
n = len(c)
|
||||
if n % 2 != 0:
|
||||
return ""
|
||||
n = n // 2
|
||||
b = bytearray(n)
|
||||
j = 0
|
||||
for i in range(0, n):
|
||||
c1 = c[j]
|
||||
c2 = c[j + 1]
|
||||
j = j + 2
|
||||
c1 = c1 - 46
|
||||
c2 = c2 - 46
|
||||
b2 = c2 * 19 + c1
|
||||
b1 = b2 ^ ksa
|
||||
b[i] = b1
|
||||
return b.decode("utf-8")
|
||||
|
||||
|
||||
def save_config(config):
|
||||
with open(config_file, 'wb') as c:
|
||||
dump(config, c)
|
||||
|
||||
|
||||
class Config:
|
||||
|
||||
def __init__(self):
|
||||
self._cookie = {}
|
||||
self._save_path = './downloads'
|
||||
self._reader_mode = False
|
||||
|
||||
def _save(self):
|
||||
with open(self._config_file, 'wb') as c:
|
||||
dump(self._config, c)
|
||||
|
||||
def encode(self, var):
|
||||
if isinstance(var, dict):
|
||||
for k, v in var.items():
|
||||
var[k] = encrypt(KEY, str(v))
|
||||
elif var:
|
||||
var = encrypt(KEY, str(var))
|
||||
return var
|
||||
|
||||
def decode(self, var):
|
||||
try:
|
||||
if isinstance(var, dict):
|
||||
dvar = {} # 新开内存,否则会修改原字典
|
||||
for k, v in var.items():
|
||||
dvar[k] = decrypt(KEY, str(v))
|
||||
elif var:
|
||||
dvar = decrypt(KEY, var)
|
||||
else:
|
||||
dvar = None
|
||||
except Exception as e:
|
||||
# print(e)
|
||||
dvar = None
|
||||
return dvar
|
||||
|
||||
@property
|
||||
def cookie(self):
|
||||
return self.decode(self._cookie)
|
||||
|
||||
@cookie.setter
|
||||
def cookie(self, value):
|
||||
self._cookie = self.encode(value)
|
||||
save_config(self)
|
||||
|
||||
@property
|
||||
def save_path(self):
|
||||
return self._save_path
|
||||
|
||||
@save_path.setter
|
||||
def save_path(self, value):
|
||||
self._save_path = value
|
||||
save_config(self)
|
||||
|
||||
@property
|
||||
def reader_mode(self):
|
||||
return self._reader_mode
|
||||
|
||||
@reader_mode.setter
|
||||
def reader_mode(self, value: bool):
|
||||
self._reader_mode = value
|
||||
save_config(self)
|
||||
|
||||
|
||||
# 全局配置对象
|
||||
try:
|
||||
with open(config_file, 'rb') as c:
|
||||
config = load(c)
|
||||
except:
|
||||
config = Config()
|
|
@ -0,0 +1,196 @@
|
|||
import os
|
||||
from platform import system as platform
|
||||
|
||||
import readline
|
||||
import requests
|
||||
|
||||
from cloud189.api import Cloud189
|
||||
from cloud189.cli import version
|
||||
|
||||
|
||||
def error(msg):
|
||||
print(f"\033[1;31mError : {msg}\033[0m")
|
||||
|
||||
|
||||
def info(msg):
|
||||
print(f"\033[1;34mInfo : {msg}\033[0m")
|
||||
|
||||
|
||||
def clear_screen():
|
||||
"""清空屏幕"""
|
||||
if os.name == 'nt':
|
||||
os.system('cls')
|
||||
else:
|
||||
os.system('clear')
|
||||
|
||||
|
||||
def why_error(code):
|
||||
"""错误原因"""
|
||||
if code == Cloud189.URL_INVALID:
|
||||
return '分享链接无效'
|
||||
elif code == Cloud189.LACK_PASSWORD:
|
||||
return '缺少提取码'
|
||||
elif code == Cloud189.PASSWORD_ERROR:
|
||||
return '提取码错误'
|
||||
elif code == Cloud189.FILE_CANCELLED:
|
||||
return '分享链接已失效'
|
||||
elif code == Cloud189.ZIP_ERROR:
|
||||
return '解压过程异常'
|
||||
elif code == Cloud189.NETWORK_ERROR:
|
||||
return '网络连接异常'
|
||||
elif code == Cloud189.CAPTCHA_ERROR:
|
||||
return '验证码错误'
|
||||
else:
|
||||
return '未知错误'
|
||||
|
||||
|
||||
def set_console_style():
|
||||
"""设置命令行窗口样式"""
|
||||
if os.name != 'nt':
|
||||
return None
|
||||
os.system('mode 120, 40')
|
||||
os.system(f'title 天翼云盘-cli {version}')
|
||||
|
||||
|
||||
def captcha_handler(img_data):
|
||||
"""处理下载时出现的验证码"""
|
||||
img_path = os.getcwd() + os.sep + 'captcha.png'
|
||||
with open(img_path, 'wb') as f:
|
||||
f.write(img_data)
|
||||
m_platform = platform()
|
||||
if m_platform == 'Darwin':
|
||||
os.system(f'open {img_path}')
|
||||
elif m_platform == 'Linux':
|
||||
os.system(f'xdg-open {img_path}')
|
||||
else:
|
||||
os.startfile(img_path)
|
||||
ans = input('\n请输入验证码:')
|
||||
os.remove(img_path)
|
||||
return ans
|
||||
|
||||
|
||||
def text_align(text, length) -> str:
|
||||
"""中英混合字符串对齐"""
|
||||
text_len = len(text)
|
||||
for char in text:
|
||||
if u'\u4e00' <= char <= u'\u9fff':
|
||||
text_len += 1
|
||||
space = length - text_len
|
||||
return text + ' ' * space
|
||||
|
||||
|
||||
def set_completer(choice_list, *, cmd_list=None, condition=None):
|
||||
"""设置自动补全"""
|
||||
if condition is None:
|
||||
condition = lambda typed, choice: choice.startswith(typed) # 默认筛选条件:选项以键入字符开头
|
||||
|
||||
def completer(typed, rank):
|
||||
tab_list = [] # TAB 补全的选项列表
|
||||
if cmd_list is not None and not typed: # 内置命令提示
|
||||
return cmd_list[rank]
|
||||
|
||||
for choice in choice_list:
|
||||
if condition(typed, choice):
|
||||
tab_list.append(choice)
|
||||
return tab_list[rank]
|
||||
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(completer)
|
||||
|
||||
|
||||
def print_logo():
|
||||
"""输出logo"""
|
||||
clear_screen()
|
||||
logo_str = f"""
|
||||
# /$$$$$$ /$$ /$$ /$$ /$$$$$$ /$$$$$$
|
||||
# /$$__ $$| $$ | $$ /$$$$ /$$__ $$ /$$__ $$
|
||||
# | $$ \__/| $$ /$$$$$$ /$$ /$$ /$$$$$$$|_ $$ | $$ \ $$| $$ \ $$
|
||||
# | $$ | $$ /$$__ $$| $$ | $$ /$$__ $$ | $$ | $$$$$$/| $$$$$$$
|
||||
# | $$ | $$| $$ \ $$| $$ | $$| $$ | $$ | $$ >$$__ $$ \____ $$
|
||||
# | $$ $$| $$| $$ | $$| $$ | $$| $$ | $$ | $$ | $$ \ $$ /$$ \ $$
|
||||
# | $$$$$$/| $$| $$$$$$/| $$$$$$/| $$$$$$$ /$$$$$$| $$$$$$/| $$$$$$/
|
||||
# \______/ |__/ \______/ \______/ \_______/|______/ \______/ \______/
|
||||
#
|
||||
--------------------------------------------------------------------------
|
||||
Github: https://github.com/rachpt/cloud189 (Version: {version})
|
||||
--------------------------------------------------------------------------
|
||||
"""
|
||||
print(logo_str)
|
||||
|
||||
|
||||
def print_help():
|
||||
clear_screen()
|
||||
help_text = f"""
|
||||
• 天翼云盘-cli v{version}
|
||||
• 支持大文件上传,无视文件格式限制
|
||||
• 支持直链提取,批量上传下载,断点续传功能
|
||||
|
||||
命令帮助 :
|
||||
help 显示本信息
|
||||
update 检查更新
|
||||
rmode 屏幕阅读器模式
|
||||
refresh 强制刷新文件列表
|
||||
xghost 清理"幽灵"文件夹
|
||||
login 使用账号密码登录网盘
|
||||
clogin 使用 Cookie 登录网盘
|
||||
logout 注销当前账号
|
||||
jobs 查看后台任务列表
|
||||
ls 列出文件(夹)
|
||||
cd 切换工作目录
|
||||
cdrec 进入回收站
|
||||
rm 删除网盘文件(夹)
|
||||
rename 重命名文件(夹)
|
||||
desc 修改文件(夹)描述
|
||||
mv 移动文件(夹)
|
||||
mkdir 创建新文件夹(最大深度 4)
|
||||
share 显示文件(夹)分享信息
|
||||
clear 清空屏幕
|
||||
clean 清空回收站
|
||||
upload 上传文件(夹)
|
||||
down 下载文件(夹),支持 URL 下载
|
||||
passwd 设置文件(夹)提取码
|
||||
setpath 设置文件下载路径
|
||||
setsize 设置单文件大小限制
|
||||
setpasswd 设置文件(夹)默认提取码
|
||||
setdelay 设置上传大文件数据块的延时
|
||||
bye 退出本程序
|
||||
|
||||
更详细的介绍请参考本项目的 Github 主页:
|
||||
https://github.com/rachpt/cloud189
|
||||
如有 Bug 反馈或建议请在 GitHub 提 Issue
|
||||
感谢您的使用 (●'◡'●)
|
||||
"""
|
||||
print(help_text)
|
||||
|
||||
|
||||
def check_update():
|
||||
"""检查更新"""
|
||||
clear_screen()
|
||||
print("正在检测更新...")
|
||||
api = "https://api.github.com/repos/rachpt/cloud189/releases/latest"
|
||||
tag_name = None
|
||||
try:
|
||||
resp = requests.get(api).json()
|
||||
tag_name, msg = resp['tag_name'], resp['body']
|
||||
update_url = resp['assets'][0]['browser_download_url']
|
||||
except (requests.RequestException, AttributeError, KeyError):
|
||||
error("检查更新时发生异常")
|
||||
input()
|
||||
return None
|
||||
if tag_name:
|
||||
ver = version.split('.')
|
||||
ver2 = tag_name.replace('v', '').split('.')
|
||||
local_version = int(ver[0]) * 100 + int(ver[1]) * 10 + int(ver[2])
|
||||
remote_version = int(ver2[0]) * 100 + int(ver2[1]) * 10 + int(ver2[2])
|
||||
if remote_version > local_version:
|
||||
print(f"程序可以更新 v{version} -> {tag_name}")
|
||||
print(f"\n@更新说明:\n{msg}")
|
||||
print(f"\n@Windows 更新:")
|
||||
print(f"Github: {update_url}")
|
||||
print("\n@Linux 更新:")
|
||||
input("git clone https://github.com/rachpt/cloud189.git")
|
||||
else:
|
||||
print("(*/ω\*) 暂无新版本发布~")
|
||||
print("但项目可能已经更新,建议去项目主页看看")
|
||||
print("如有 Bug 或建议,请提 Issue")
|
||||
print("Github: https://github.com/rachpt/cloud189")
|
|
@ -1,3 +1,7 @@
|
|||
certifi
|
||||
chardet
|
||||
idna
|
||||
pyreadline
|
||||
requests
|
||||
requests_toolbelt
|
||||
rsa
|
BIN
src/view.png
BIN
src/view.png
Binary file not shown.
Before Width: | Height: | Size: 334 KiB |
Loading…
Reference in New Issue