use aligo

This commit is contained in:
jxxghp
2024-09-25 18:44:18 +08:00
parent 41d41685fe
commit 2da95fa4e6
8 changed files with 203 additions and 634 deletions

View File

@@ -34,6 +34,7 @@ RUN apt-get update -y \
rsync \
ffmpeg \
nano \
aria2 \
&& \
if [ "$(uname -m)" = "x86_64" ]; \
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \

View File

@@ -40,11 +40,11 @@ class StorageChain(ChainBase):
"""
return self.run_module("create_folder", fileitem=fileitem, name=name)
def download_file(self, fileitem: schemas.FileItem) -> Optional[Path]:
def download_file(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
下载文件
"""
return self.run_module("download_file", fileitem=fileitem)
return self.run_module("download_file", fileitem=fileitem, path=path)
def upload_file(self, fileitem: schemas.FileItem, path: Path) -> Optional[bool]:
"""

View File

@@ -206,7 +206,7 @@ class FileManagerModule(_ModuleBase):
return False
return storage_oper.rename(fileitem, name)
def download_file(self, fileitem: FileItem) -> Optional[Path]:
def download_file(self, fileitem: FileItem, path: Path = None) -> Optional[Path]:
"""
下载文件
"""
@@ -214,7 +214,7 @@ class FileManagerModule(_ModuleBase):
if not storage_oper:
logger.error(f"不支持 {fileitem.storage} 的下载处理")
return None
return storage_oper.download(fileitem)
return storage_oper.download(fileitem, path=path)
def upload_file(self, fileitem: FileItem, path: Path) -> Optional[FileItem]:
"""
@@ -449,7 +449,7 @@ class FileManagerModule(_ModuleBase):
# 网盘到本地
if transfer_type in ["copy", "move"]:
# 下载
tmp_file = source_oper.download(fileitem)
tmp_file = source_oper.download(fileitem=fileitem, path=target_file.parent)
if tmp_file:
# 创建目录
if not target_file.parent.exists():
@@ -464,29 +464,25 @@ class FileManagerModule(_ModuleBase):
return None, f"{fileitem.path} {fileitem.storage} 下载失败"
elif fileitem.storage == target_storage:
# 同一网盘
if transfer_type == "move":
# 移动
# 根据目的路径获取文件夹
target_diritem = target_oper.get_folder(target_file.parent)
if target_diritem:
# 重命名文件
if target_oper.rename(fileitem, target_file.name):
# 移动文件到新目录
if source_oper.move(fileitem, target_diritem):
ret_fileitem = copy.deepcopy(fileitem)
ret_fileitem.path = target_diritem.path + "/" + target_file.name
ret_fileitem.name = target_file.name
ret_fileitem.basename = target_file.stem
ret_fileitem.parent_fileid = target_diritem.fileid
return ret_fileitem, ""
else:
return None, f"{fileitem.path} {target_storage} 移动文件失败"
# 根据目的路径获取文件夹
target_diritem = target_oper.get_folder(target_file.parent)
if target_diritem:
# 重命名文件
if target_oper.rename(fileitem, target_file.name):
# 移动文件到新目录
if source_oper.move(fileitem, target_diritem):
ret_fileitem = copy.deepcopy(fileitem)
ret_fileitem.path = target_diritem.path + "/" + target_file.name
ret_fileitem.name = target_file.name
ret_fileitem.basename = target_file.stem
ret_fileitem.parent_fileid = target_diritem.fileid
return ret_fileitem, ""
else:
return None, f"{fileitem.path} {target_storage} 重命名文件失败"
return None, f"{fileitem.path} {target_storage} 移动文件失败"
else:
return None, f"{target_file.parent} {target_storage} 目录获取失败"
return None, f"{fileitem.path} {target_storage} 重命名文件失败"
else:
return None, f"网盘内只支持移动操作"
return None, f"{target_file.parent} {target_storage} 目录获取失败"
return None, "未知错误"

View File

@@ -54,14 +54,14 @@ class StorageBase(metaclass=ABCMeta):
pass
@abstractmethod
def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
"""
浏览文件
"""
pass
@abstractmethod
def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
"""
创建目录
"""
@@ -82,63 +82,63 @@ class StorageBase(metaclass=ABCMeta):
pass
@abstractmethod
def delete(self, fileitm: schemas.FileItem) -> bool:
def delete(self, fileitem: schemas.FileItem) -> bool:
"""
删除文件
"""
pass
@abstractmethod
def rename(self, fileitm: schemas.FileItem, name: str) -> bool:
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
"""
重命名文件
"""
pass
@abstractmethod
def download(self, fileitm: schemas.FileItem) -> Path:
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Path:
"""
下载文件,保存到本地,返回本地临时文件地址
"""
pass
@abstractmethod
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
"""
上传文件
"""
pass
@abstractmethod
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
"""
获取文件详情
"""
pass
@abstractmethod
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def copy(self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool:
"""
复制文件
"""
pass
@abstractmethod
def move(self, fileitm: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool:
def move(self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool:
"""
移动文件
"""
pass
@abstractmethod
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
"""
硬链接文件
"""
pass
@abstractmethod
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
"""
软链接文件
"""

View File

@@ -1,21 +1,20 @@
import base64
import hashlib
import json
import logging
import subprocess
import time
import uuid
from pathlib import Path
from typing import Optional, Tuple, List
from requests import Response
from app import schemas
from app.core.config import settings
from app.log import logger
from app.modules.filemanager.storages import StorageBase
from app.schemas.types import StorageSchema
from app.utils.http import RequestUtils
from aligo import Aligo, BaseFile
from app.utils.string import StringUtils
from app.utils.system import SystemUtils
class AliPan(StorageBase):
@@ -28,14 +27,14 @@ class AliPan(StorageBase):
# 支持的整理方式
transtype = {
"move": "移动"
"move": "移动",
}
_X_SIGNATURE = ('f4b7bed5d8524a04051bd2da876dd79afe922b8205226d65855d02b267422adb1'
'e0d8a816b021eaf5c36d101892180f79df655c5712b348c2a540ca136e6b22001')
# 是否有aria2c
_has_aria2c: bool = False
_X_PUBLIC_KEY = ('04d9d2319e0480c840efeeb75751b86d0db0c5b9e72c6260a1d846958adceaf9d'
'ee789cab7472741d23aafc1a9c591f72e7ee77578656e6c8588098dea1488ac2a')
# aligo
aligo: Aligo = None
# 生成二维码
qrcode_url = ("https://passport.aliyundrive.com/newlogin/qrcode/generate.do?"
@@ -43,62 +42,27 @@ class AliPan(StorageBase):
"&lang=zh_CN&returnUrl=&bizParams=&_bx-v=2.0.31")
# 二维码登录确认
check_url = "https://passport.aliyundrive.com/newlogin/qrcode/query.do?appName=aliyun_drive&fromSite=52&_bx-v=2.0.31"
# 更新访问令牌
update_accessstoken_url = "https://auth.aliyundrive.com/v2/account/token"
# 创建会话
create_session_url = "https://api.aliyundrive.com/users/v1/users/device/create_session"
# 用户信息
user_info_url = "https://user.aliyundrive.com/v2/user/get"
# 浏览文件
list_file_url = "https://api.aliyundrive.com/adrive/v3/file/list"
# 创建目录或文件
create_folder_file_url = "https://api.aliyundrive.com/adrive/v2/file/createWithFolders"
# 文件详情
file_detail_url = "https://api.aliyundrive.com/v2/file/get"
# 删除文件
delete_file_url = " https://api.aliyundrive.com/v2/recyclebin/trash"
# 文件重命名
rename_file_url = "https://api.aliyundrive.com/v3/file/update"
# 获取下载链接
download_url = "https://api.aliyundrive.com/v2/file/get_download_url"
# 移动文件
move_file_url = "https://api.aliyundrive.com/v2/file/move"
# 上传文件完成
upload_file_complete_url = "https://api.aliyundrive.com/v2/file/complete"
# 查询存储详情
storage_info_url = "https://api.aliyundrive.com/adrive/v1/user/driveCapacityDetails"
# 播放地址
play_info_url = 'https://api.aliyundrive.com/v2/file/get_video_preview_play_info'
def __handle_error(self, res: Response, apiname: str, action: bool = True):
"""
统一处理和打印错误信息
"""
if res is None:
logger.warn("无法连接到阿里云盘!")
return
def __init__(self):
super().__init__()
try:
result = res.json()
except Exception as err:
logger.error(f"解析阿里云盘返回数据失败:{str(err)}")
return
code = result.get("code")
message = result.get("message")
display_message = result.get("display_message")
if code or message:
logger.warn(f"Aliyun {apiname}失败:{code} - {display_message or message}")
if action:
if code == "DeviceSessionSignatureInvalid":
logger.warn("设备已失效,正在重新建立会话...")
self.__create_session(self.__get_headers(self.__auth_params))
if code == "UserDeviceOffline":
logger.warn("设备已离线,尝试重新登录,如仍报错请检查阿里云盘绑定设备数量是否超限!")
self.__create_session(self.__get_headers(self.__auth_params))
if code == "AccessTokenInvalid":
logger.warn("访问令牌已失效,正在刷新令牌...")
self.__update_accesstoken(self.__auth_params, self.__auth_params.get("refreshToken"))
else:
logger.info(f"Aliyun {apiname}成功")
subprocess.run(['aria2c', '-h'], capture_output=True)
self._has_aria2c = True
logger.debug('发现 aria2c, 将使用 aria2c 下载文件')
except FileNotFoundError:
logger.debug('未发现 aria2c')
self._has_aria2c = False
self.__init_aligo()
def __init_aligo(self):
"""
初始化 aligo
"""
refresh_token = self.__auth_params.get("refreshToken")
if refresh_token:
self.aligo = Aligo(refresh_token=refresh_token, use_aria2=self._has_aria2c,
name="MoviePilot V2", level=logging.ERROR)
@property
def __auth_params(self):
@@ -135,7 +99,6 @@ class AliPan(StorageBase):
"t": data.get("t")
}, ""
elif res is not None:
self.__handle_error(res, "生成二维码")
return {}, f"请求阿里云盘二维码失败:{res.status_code} - {res.reason}"
return {}, f"请求阿里云盘二维码失败:无法连接!"
@@ -194,151 +157,62 @@ class AliPan(StorageBase):
"updateTime": time.time(),
})
self.__update_params(data)
self.user_info()
self.__init_aligo()
except Exception as e:
return {}, f"bizExt 解码失败:{str(e)}"
return data, ""
elif res is not None:
self.__handle_error(res, "登录确认")
return {}, f"阿里云盘登录确认失败:{res.status_code} - {res.reason}"
return {}, "阿里云盘登录确认失败:无法连接!"
def __update_accesstoken(self, params: dict, refresh_token: str) -> bool:
"""
更新阿里云盘访问令牌
"""
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(
self.update_accessstoken_url, json={
"refresh_token": refresh_token,
"grant_type": "refresh_token"
})
if res:
data = res.json()
code = data.get("code")
if code in ["RefreshTokenExpired", "InvalidParameter.RefreshToken"]:
logger.warn("刷新令牌已过期,请重新登录!")
self.__clear_params()
return False
self.__update_params({
"accessToken": data.get('access_token'),
"expiresIn": data.get('expires_in'),
"updateTime": time.time()
})
logger.info(f"阿里云盘访问令牌已更新accessToken={data.get('access_token')}")
return True
else:
self.__handle_error(res, "更新令牌", action=False)
return False
def __create_session(self, headers: dict):
"""
创建会话
"""
def __os_name():
"""
获取操作系统名称
"""
if SystemUtils.is_windows():
return 'Windows 操作系统'
elif SystemUtils.is_macos():
return 'MacOS 操作系统'
else:
return '类 Unix 操作系统'
res = RequestUtils(headers=headers, timeout=5).post_res(self.create_session_url, json={
'deviceName': 'MoviePilot',
'modelName': __os_name(),
'pubKey': self._X_PUBLIC_KEY,
})
self.__handle_error(res, "创建会话", action=False)
@property
def __access_params(self) -> Optional[dict]:
"""
获取阿里云盘访问参数,如果超时则更新后返回
"""
params = self.__auth_params
if not params:
logger.warn("阿里云盘访问令牌不存在,请先扫码登录!")
return None
expires_in = params.get("expiresIn")
update_time = params.get("updateTime")
refresh_token = params.get("refreshToken")
if not expires_in or not update_time or not refresh_token:
logger.warn("阿里云盘访问令牌参数错误,请重新扫码登录!")
self.__clear_params()
return None
# 是否需要更新设备信息
update_device = False
# 判断访问令牌是否过期
if (time.time() - update_time) >= expires_in:
logger.info("阿里云盘访问令牌已过期,正在更新...")
if not self.__update_accesstoken(params, refresh_token):
# 更新失败
return None
update_device = True
# 生成设备ID
x_device_id = params.get("x_device_id")
if not x_device_id:
x_device_id = uuid.uuid4().hex
params['x_device_id'] = x_device_id
self.__update_params({"x_device_id": x_device_id})
update_device = True
# 更新设备信息重新创建会话
if update_device:
self.__create_session(self.__get_headers(params))
return params
def __get_headers(self, params: dict):
"""
获取请求头
"""
if not params:
return {}
return {
"Authorization": f"Bearer {params.get('accessToken')}",
"Content-Type": "application/json;charset=UTF-8",
"Accept": "application/json, text/plain, */*",
"Referer": "https://www.alipan.com/",
"User-Agent": settings.USER_AGENT,
"X-Canary": "client=web,app=adrive,version=v4.9.0",
"x-device-id": params.get('x_device_id'),
"x-signature": self._X_SIGNATURE
}
def check(self) -> bool:
"""
检查存储是否可用
"""
params = self.__access_params
if not params:
if not self.aligo:
return False
return True if self.list(schemas.FileItem(
fileid="root",
drive_id=params.get("resourceDriveId")
)) else False
return True if self.aligo.get_user() else False
def user_info(self) -> dict:
"""
获取用户信息drive_id等
"""
params = self.__access_params
if not params:
return {}
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.user_info_url)
if res:
result = res.json()
self.__update_params({
"resourceDriveId": result.get("resource_drive_id"),
"backDriveId": result.get("backup_drive_id")
})
return result
return self.aligo.get_user()
def __get_fileitem(self, fileinfo: BaseFile, parent: str = "/") -> schemas.FileItem:
"""
获取文件信息
"""
if not fileinfo:
return schemas.FileItem()
if fileinfo.type == "folder":
return schemas.FileItem(
storage=self.schema.value,
fileid=fileinfo.file_id,
parent_fileid=fileinfo.parent_file_id,
type="dir",
path=f"{parent}{fileinfo.name}" + "/",
name=fileinfo.name,
basename=fileinfo.name,
size=fileinfo.size,
modify_time=StringUtils.str_to_timestamp(fileinfo.updated_at),
drive_id=fileinfo.drive_id,
)
else:
self.__handle_error(res, "获取用户信息")
return {}
return schemas.FileItem(
storage=self.schema.value,
fileid=fileinfo.file_id,
parent_fileid=fileinfo.parent_file_id,
type="file",
path=f"{parent}{fileinfo.name}",
name=fileinfo.name,
basename=Path(fileinfo.name).stem,
size=fileinfo.size,
extension=fileinfo.file_extension,
modify_time=StringUtils.str_to_timestamp(fileinfo.updated_at),
thumbnail=fileinfo.thumbnail,
drive_id=fileinfo.drive_id,
)
def list(self, fileitem: schemas.FileItem = None) -> List[schemas.FileItem]:
"""
@@ -348,18 +222,15 @@ class AliPan(StorageBase):
parent_file_id 根目录为root
type all | file | folder
"""
params = self.__access_params
if not params:
if not self.aligo:
return []
# 请求头
headers = self.__get_headers(params)
# 根目录处理
if not fileitem or not fileitem.drive_id:
return [
schemas.FileItem(
storage=self.schema.value,
fileid=fileitem.fileid,
drive_id=params.get("resourceDriveId"),
drive_id=self.__auth_params.get("resourceDriveId"),
parent_fileid="root",
type="dir",
path="/资源库/",
@@ -369,7 +240,7 @@ class AliPan(StorageBase):
schemas.FileItem(
storage=self.schema.value,
fileid=fileitem.fileid,
drive_id=params.get("backDriveId"),
drive_id=self.__auth_params.get("backDriveId"),
parent_fileid="root",
type="dir",
path="/备份盘/",
@@ -377,436 +248,126 @@ class AliPan(StorageBase):
basename="备份盘"
)
]
# 如果本身是文件
if fileitem.type == "file":
return [fileitem]
# 返回数据
ret_items = []
# 分页获取
next_marker = None
while True:
if not fileitem.parent_fileid or fileitem.parent_fileid == "/":
parent_file_id = "root"
else:
parent_file_id = fileitem.fileid
res = RequestUtils(headers=headers, timeout=10).post_res(self.list_file_url, json={
"drive_id": fileitem.drive_id,
"parent_file_id": parent_file_id,
"marker": next_marker
}, params={
'jsonmask': ('next_marker,items(name,file_id,drive_id,type,size,created_at,updated_at,'
'category,file_extension,parent_file_id,mime_type,starred,thumbnail,url,'
'streams_info,content_hash,user_tags,user_meta,trashed,video_media_metadata,'
'video_preview_metadata,sync_meta,sync_device_flag,sync_flag,punish_flag')
})
if res:
result = res.json()
items = result.get("items")
if not items:
break
# 合并数据
ret_items.extend(items)
next_marker = result.get("next_marker")
if not next_marker:
# 没有下一页
break
else:
self.__handle_error(res, "浏览文件")
break
return [schemas.FileItem(
storage=self.schema.value,
fileid=fileinfo.get("file_id"),
parent_fileid=fileinfo.get("parent_file_id"),
type="dir" if fileinfo.get("type") == "folder" else "file",
path=f"{fileitem.path}{fileinfo.get('name')}" + ("/" if fileinfo.get("type") == "folder" else ""),
name=fileinfo.get("name"),
basename=Path(fileinfo.get("name")).stem,
size=fileinfo.get("size"),
extension=fileinfo.get("file_extension"),
modify_time=StringUtils.str_to_timestamp(fileinfo.get("updated_at")),
thumbnail=fileinfo.get("thumbnail"),
drive_id=fileinfo.get("drive_id"),
) for fileinfo in ret_items]
elif fileitem.type == "file":
# 文件处理
file = self.detail(fileitem)
if file:
return [file]
else:
items = self.aligo.get_file_list(parent_file_id=fileitem.fileid, drive_id=fileitem.drive_id)
if items:
return [self.__get_fileitem(item) for item in items]
return []
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
"""
创建目录
"""
params = self.__access_params
if not params:
if not self.aligo:
return None
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.create_folder_file_url, json={
"drive_id": fileitem.drive_id,
"parent_file_id": fileitem.parent_fileid,
"name": name,
"check_name_mode": "refuse",
"type": "folder"
})
if res:
"""
{
"parent_file_id": "root",
"type": "folder",
"file_id": "6673f2c8a88344741bd64ad192d7512b92087719",
"domain_id": "bj29",
"drive_id": "39146740",
"file_name": "test",
"encrypt_mode": "none"
}
"""
result = res.json()
return schemas.FileItem(
storage=self.schema.value,
fileid=result.get("file_id"),
drive_id=result.get("drive_id"),
parent_fileid=result.get("parent_file_id"),
type=result.get("type"),
name=result.get("file_name"),
path=f"{fileitem.path}{result.get('file_name')}",
)
else:
self.__handle_error(res, "创建目录")
item = self.aligo.create_folder(name=name, parent_file_id=fileitem.fileid, drive_id=fileitem.drive_id)
if item:
return self.__get_fileitem(item)
return None
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
"""
根据文件路程获取目录,不存在则创建
"""
def __find_dir(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]:
"""
查找下级目录中匹配名称的目录
"""
for sub_file in self.list(_fileitem):
if sub_file.type != "dir":
continue
if sub_file.name == _name:
return sub_file
if not self.aligo:
return None
# 逐级查找和创建目录
fileitem = schemas.FileItem(fileid="root")
for part in path.parts:
if part == "/":
continue
dir_file = __find_dir(fileitem, part)
if dir_file:
fileitem = dir_file
else:
dir_file = self.create_folder(dir_file, part)
if not dir_file:
logger.warn(f"{self.schema.value}创建目录 {fileitem.path}{part} 失败!")
return None
fileitem = dir_file
return fileitem
item = self.aligo.get_folder_by_path(path=str(Path), create_folder=True)
if item:
return self.__get_fileitem(item)
return None
def get_item(self, path: Path) -> Optional[schemas.FileItem]:
"""
获取文件或目录不存在返回None
"""
def __find_item(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]:
"""
查找下级目录中匹配名称的目录或文件
"""
for sub_file in self.list(_fileitem):
if sub_file.name == _name:
return sub_file
if not self.aligo:
return None
# 逐级查找和创建目录
fileitem = schemas.FileItem(fileid="root")
for part in path.parts:
if part == "/":
continue
item = __find_item(fileitem, part)
if not item:
return None
fileitem = item
return fileitem
item = self.aligo.get_file_by_path(path=str(path))
if item:
return self.__get_fileitem(item)
return None
def delete(self, fileitem: schemas.FileItem) -> bool:
"""
删除文件
"""
params = self.__access_params
if not params:
if not self.aligo:
return False
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.delete_file_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid
})
if res:
if self.aligo.move_file_to_trash(file_id=fileitem.fileid, drive_id=fileitem.drive_id):
return True
else:
self.__handle_error(res, "删除文件")
return False
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
"""
获取文件详情
"""
params = self.__access_params
if not params:
if not self.aligo:
return None
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.file_detail_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid
})
if res:
result = res.json()
return schemas.FileItem(
storage=self.schema.value,
fileid=result.get("file_id"),
drive_id=result.get("drive_id"),
parent_fileid=result.get("parent_file_id"),
type="file",
name=result.get("name"),
size=result.get("size"),
extension=result.get("file_extension"),
modify_time=StringUtils.str_to_timestamp(result.get("updated_at")),
thumbnail=result.get("thumbnail"),
path=f"{fileitem.path}{result.get('name')}",
url=result.get("download_url") or result.get("url")
)
else:
self.__handle_error(res, "获取文件详情")
item = self.aligo.get_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id)
if item:
return self.__get_fileitem(item)
return None
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
"""
重命名文件
"""
params = self.__access_params
if not params:
if not self.aligo:
return False
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.rename_file_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid,
"name": name,
"check_name_mode": "refuse"
})
if res:
if self.aligo.rename_file(file_id=fileitem.fileid, name=name, drive_id=fileitem.drive_id):
return True
else:
self.__handle_error(res, "重命名文件")
return False
def download(self, fileitem: schemas.FileItem) -> Optional[Path]:
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
下载文件,保存到本地
"""
params = self.__access_params
if not params:
if not self.aligo:
return None
headers = self.__get_headers(params)
def __get_play_url():
"""
获取播放地址
"""
play_res = RequestUtils(headers=headers, timeout=10).post_res(self.play_info_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid
})
if play_res:
play_dict = {}
play_info = play_res.json()
if play_info.get('video_preview_play_info'):
for i in play_info['video_preview_play_info'].get('live_transcoding_task_list') or []:
if i.get('url'):
try:
play_dict[i['template_id']] = i['url']
except KeyError:
pass
if play_dict:
return list(play_dict.values())[-1]
return None
# 先获取文件详情
fileinfo = self.detail(fileitem)
if not fileinfo:
logger.warn(f"{fileitem.path} 文件不存在")
return None
# 文件下载链接
download_url = None
if fileinfo.url:
# 使用文件详情中的链接
download_url = fileinfo.url
else:
# 查询文件下载链接
res = RequestUtils(headers=headers, timeout=10).post_res(self.download_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid,
"file_name": fileitem.name,
})
if res:
result = res.json()
download_url = result.get("url") or result.get("internal_url")
if not download_url:
# 查询播放链接
download_url = __get_play_url()
if not download_url:
logger.warn(f"{fileitem.path} 未获取到下载链接")
return None
# 下载文件到本地
res = RequestUtils(headers={
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Referer": "https://www.alipan.com/",
"Sec-Fetch-Dest": "iframe",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "cross-site",
"User-Agent": settings.USER_AGENT
}).get_res(download_url)
if res:
path = settings.TEMP_PATH / fileitem.name
with path.open("wb") as f:
f.write(res.content)
return path
else:
self.__handle_error(res, "获取下载链接")
local_path = self.aligo.download_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id,
local_folder=str(path or settings.TEMP_PATH))
if local_path:
return Path(local_path)
return None
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
"""
上传文件,并标记完成
"""
__UPLOAD_CHUNK_SIZE: int = 10485760 # 10 MB
def __sha1(_path: Path):
"""
计算文件sha1用于快传
"""
_sha1 = hashlib.sha1()
with open(_path, 'rb') as f:
while True:
data = f.read(8192)
if not data:
break
_sha1.update(data)
return _sha1.hexdigest()
params = self.__access_params
if not params:
if not self.aligo:
return None
headers = self.__get_headers(params)
# 计算sha1
sha1 = __sha1(path)
res = RequestUtils(headers=headers, timeout=10).post_res(self.create_folder_file_url, json={
"drive_id": fileitem.drive_id,
"parent_file_id": fileitem.parent_fileid,
"name": path.name,
"check_name_mode": "refuse",
"create_scene": "file_upload",
"type": "file",
"content_hash": sha1,
"content_hash_name": "sha1",
"part_info_list": [
{
"part_number": 1
}
],
"size": path.stat().st_size
})
if not res:
self.__handle_error(res, "创建文件")
return None
# 获取上传请求结果
result = res.json()
if result.get("exist") or result.get("rapid_upload"):
# 已存在
logger.info(f"文件 {result.get('file_name')} 已存在或已秒传完成,无需上传")
return schemas.FileItem(
storage=self.schema.value,
drive_id=result.get("drive_id"),
fileid=result.get("file_id"),
parent_fileid=result.get("parent_file_id"),
type="file",
name=result.get("file_name"),
path=f"{fileitem.path}{result.get('file_name')}"
)
# 上传文件
file_id = result.get("file_id")
upload_id = result.get("upload_id")
part_info_list = result.get("part_info_list")
if part_info_list:
# 上传地址
upload_url = part_info_list[0].get("upload_url")
# 上传文件
res = RequestUtils(headers={
"Content-Type": "",
"User-Agent": settings.USER_AGENT,
"Referer": "https://www.alipan.com/",
"Accept": "*/*",
}).put_res(upload_url, data=path.read_bytes())
if not res:
self.__handle_error(res, "上传文件")
return None
# 标记文件上传完毕
res = RequestUtils(headers=headers, timeout=10).post_res(self.upload_file_complete_url, json={
"drive_id": fileitem.drive_id,
"file_id": file_id,
"upload_id": upload_id
})
if not res:
self.__handle_error(res, "标记上传状态")
return None
result = res.json()
return schemas.FileItem(
storage=self.schema.value,
fileid=result.get("file_id"),
drive_id=result.get("drive_id"),
parent_fileid=result.get("parent_file_id"),
type="file",
name=result.get("name"),
path=f"{fileitem.path}{result.get('name')}",
)
else:
logger.warn("阿里云盘上传文件失败:无法获取上传地址!")
item = self.aligo.upload_file(file_path=str(path.parent), parent_file_id=fileitem.fileid,
drive_id=fileitem.drive_id, name=path.name)
if item:
return self.__get_fileitem(item)
return None
def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool:
"""
移动文件
"""
params = self.__access_params
if not params:
if not self.aligo:
return False
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.move_file_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid,
"to_parent_file_id": target.fileid,
"check_name_mode": "refuse"
})
if res:
if self.aligo.move_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id,
to_parent_file_id=target.fileid, to_drive_id=target.drive_id):
return True
else:
self.__handle_error(res, "移动文件")
return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
"""
复制文件
"""
def copy(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool:
pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
"""
硬链接文件
"""
pass
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
"""
软链接文件
"""
@@ -816,17 +377,14 @@ class AliPan(StorageBase):
"""
存储使用情况
"""
params = self.__access_params
if not params:
if not self.aligo:
return None
headers = self.__get_headers(params)
res = RequestUtils(headers=headers, timeout=10).post_res(self.storage_info_url, json={})
if res:
result = res.json()
return schemas.StorageUsage(
total=result.get("drive_total_size"),
available=result.get("drive_total_size") - result.get("drive_used_size")
)
else:
self.__handle_error(res, "查询存储详情")
user_capacity = self.aligo.get_user_capacity_info()
if user_capacity:
drive_capacity = user_capacity.drive_capacity_details
if drive_capacity:
return schemas.StorageUsage(
total=drive_capacity.drive_total_size,
available=drive_capacity.drive_total_size - drive_capacity.drive_used_size
)
return None

View File

@@ -135,11 +135,11 @@ class LocalStorage(StorageBase):
return self.__get_fileitem(path)
return self.__get_diritem(path)
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
"""
获取文件详情
"""
path_obj = Path(fileitm.path)
path_obj = Path(fileitem.path)
if not path_obj.exists():
return None
return self.__get_fileitem(path_obj)
@@ -177,7 +177,7 @@ class LocalStorage(StorageBase):
return False
return True
def download(self, fileitem: schemas.FileItem) -> Optional[Path]:
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
下载文件
"""

View File

@@ -118,7 +118,7 @@ class Rclone(StorageBase):
logger.error(f"rclone浏览文件失败{err}")
return []
def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
"""
创建目录
"""
@@ -126,13 +126,13 @@ class Rclone(StorageBase):
retcode = subprocess.run(
[
'rclone', 'mkdir',
f'MP:{fileitm.path}/{name}'
f'MP:{fileitem.path}/{name}'
],
startupinfo=self.__get_hidden_shell()
).returncode
if retcode == 0:
ret_fileitem = copy.deepcopy(fileitm)
ret_fileitem.path = f"{fileitm.path}/{name}/"
ret_fileitem = copy.deepcopy(fileitem)
ret_fileitem.path = f"{fileitem.path}/{name}/"
ret_fileitem.name = name
return ret_fileitem
except Exception as err:
@@ -191,7 +191,7 @@ class Rclone(StorageBase):
logger.error(f"rclone获取文件失败{err}")
return None
def delete(self, fileitm: schemas.FileItem) -> bool:
def delete(self, fileitem: schemas.FileItem) -> bool:
"""
删除文件
"""
@@ -199,7 +199,7 @@ class Rclone(StorageBase):
retcode = subprocess.run(
[
'rclone', 'deletefile',
f'MP:{fileitm.path}'
f'MP:{fileitem.path}'
],
startupinfo=self.__get_hidden_shell()
).returncode
@@ -209,7 +209,7 @@ class Rclone(StorageBase):
logger.error(f"rclone删除文件失败{err}")
return False
def rename(self, fileitm: schemas.FileItem, name: str) -> bool:
def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
"""
重命名文件
"""
@@ -217,8 +217,8 @@ class Rclone(StorageBase):
retcode = subprocess.run(
[
'rclone', 'moveto',
f'MP:{fileitm.path}',
f'MP:{Path(fileitm.path).parent}/{name}'
f'MP:{fileitem.path}',
f'MP:{Path(fileitem.path).parent}/{name}'
],
startupinfo=self.__get_hidden_shell()
).returncode
@@ -228,11 +228,11 @@ class Rclone(StorageBase):
logger.error(f"rclone重命名文件失败{err}")
return False
def download(self, fileitem: schemas.FileItem) -> Optional[Path]:
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
下载文件
"""
path = settings.TEMP_PATH / fileitem.name
path = (path or settings.TEMP_PATH) / fileitem.name
try:
retcode = subprocess.run(
[
@@ -248,7 +248,7 @@ class Rclone(StorageBase):
logger.error(f"rclone复制文件失败{err}")
return None
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
"""
上传文件
"""
@@ -256,7 +256,7 @@ class Rclone(StorageBase):
retcode = subprocess.run(
[
'rclone', 'copyto',
fileitm.path,
fileitem.path,
f'MP:{path}'
],
startupinfo=self.__get_hidden_shell()
@@ -267,7 +267,7 @@ class Rclone(StorageBase):
logger.error(f"rclone上传文件失败{err}")
return None
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
"""
获取文件详情
"""
@@ -275,7 +275,7 @@ class Rclone(StorageBase):
ret = subprocess.run(
[
'rclone', 'lsjson',
f'MP:{fileitm.path}'
f'MP:{fileitem.path}'
],
capture_output=True,
startupinfo=self.__get_hidden_shell()
@@ -287,7 +287,7 @@ class Rclone(StorageBase):
logger.error(f"rclone获取文件详情失败{err}")
return None
def move(self, fileitm: schemas.FileItem, target: Path) -> bool:
def move(self, fileitem: schemas.FileItem, target: Path) -> bool:
"""
移动文件target_file格式rclone:path
"""
@@ -295,7 +295,7 @@ class Rclone(StorageBase):
retcode = subprocess.run(
[
'rclone', 'moveto',
f'MP:{fileitm.path}',
f'MP:{fileitem.path}',
f'MP:{target}'
],
startupinfo=self.__get_hidden_shell()
@@ -306,13 +306,13 @@ class Rclone(StorageBase):
logger.error(f"rclone移动文件失败{err}")
return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def usage(self) -> Optional[schemas.StorageUsage]:

View File

@@ -1,4 +1,5 @@
import base64
import subprocess
from pathlib import Path
from typing import Optional, Tuple, List
@@ -32,6 +33,19 @@ class U115Pan(StorageBase, metaclass=Singleton):
cloud: Optional[Cloud] = None
_session: QrcodeSession = None
# 是否有aria2c
_has_aria2c: bool = False
def __init__(self):
super().__init__()
try:
subprocess.run(['aria2c', '-h'], capture_output=True)
self._has_aria2c = True
logger.debug('发现 aria2c, 将使用 aria2c 下载文件')
except FileNotFoundError:
logger.debug('未发现 aria2c')
self._has_aria2c = False
def __init_cloud(self) -> bool:
"""
初始化Cloud
@@ -260,7 +274,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
fileitem = item
return fileitem
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
"""
获取文件详情
"""
@@ -292,7 +306,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
logger.error(f"115重命名文件失败{str(e)}")
return False
def download(self, fileitem: schemas.FileItem) -> Optional[Path]:
def download(self, fileitem: schemas.FileItem, path: Path = None) -> Optional[Path]:
"""
获取下载链接
"""
@@ -301,7 +315,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
try:
ticket = self.cloud.storage().request_download(fileitem.pickcode)
if ticket:
path = settings.TEMP_PATH / fileitem.name
path = (path or settings.TEMP_PATH) / fileitem.name
res = RequestUtils(headers=ticket.headers).get_res(ticket.url)
if res:
with open(path, "wb") as f:
@@ -374,13 +388,13 @@ class U115Pan(StorageBase, metaclass=Singleton):
logger.error(f"115移动文件失败{str(e)}")
return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass
def usage(self) -> Optional[schemas.StorageUsage]: