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 \ rsync \
ffmpeg \ ffmpeg \
nano \ nano \
aria2 \
&& \ && \
if [ "$(uname -m)" = "x86_64" ]; \ if [ "$(uname -m)" = "x86_64" ]; \
then ln -s /usr/lib/x86_64-linux-musl/libc.so /lib/libc.musl-x86_64.so.1; \ 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) 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]: def upload_file(self, fileitem: schemas.FileItem, path: Path) -> Optional[bool]:
""" """

View File

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

View File

@@ -54,14 +54,14 @@ class StorageBase(metaclass=ABCMeta):
pass pass
@abstractmethod @abstractmethod
def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]: def list(self, fileitem: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
""" """
浏览文件 浏览文件
""" """
pass pass
@abstractmethod @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 pass
@abstractmethod @abstractmethod
def delete(self, fileitm: schemas.FileItem) -> bool: def delete(self, fileitem: schemas.FileItem) -> bool:
""" """
删除文件 删除文件
""" """
pass pass
@abstractmethod @abstractmethod
def rename(self, fileitm: schemas.FileItem, name: str) -> bool: def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
""" """
重命名文件 重命名文件
""" """
pass pass
@abstractmethod @abstractmethod
def download(self, fileitm: schemas.FileItem) -> Path: def download(self, fileitem: schemas.FileItem, path: Path = None) -> Path:
""" """
下载文件,保存到本地,返回本地临时文件地址 下载文件,保存到本地,返回本地临时文件地址
""" """
pass pass
@abstractmethod @abstractmethod
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]: def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
""" """
上传文件 上传文件
""" """
pass pass
@abstractmethod @abstractmethod
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]: def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
""" """
获取文件详情 获取文件详情
""" """
pass pass
@abstractmethod @abstractmethod
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def copy(self, fileitem: schemas.FileItem, target: Union[schemas.FileItem, Path]) -> bool:
""" """
复制文件 复制文件
""" """
pass pass
@abstractmethod @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 pass
@abstractmethod @abstractmethod
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
""" """
硬链接文件 硬链接文件
""" """
pass pass
@abstractmethod @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 base64
import hashlib
import json import json
import logging
import subprocess
import time import time
import uuid
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
from requests import Response
from app import schemas from app import schemas
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
from app.modules.filemanager.storages import StorageBase from app.modules.filemanager.storages import StorageBase
from app.schemas.types import StorageSchema from app.schemas.types import StorageSchema
from app.utils.http import RequestUtils from app.utils.http import RequestUtils
from aligo import Aligo, BaseFile
from app.utils.string import StringUtils from app.utils.string import StringUtils
from app.utils.system import SystemUtils
class AliPan(StorageBase): class AliPan(StorageBase):
@@ -28,14 +27,14 @@ class AliPan(StorageBase):
# 支持的整理方式 # 支持的整理方式
transtype = { transtype = {
"move": "移动" "move": "移动",
} }
_X_SIGNATURE = ('f4b7bed5d8524a04051bd2da876dd79afe922b8205226d65855d02b267422adb1' # 是否有aria2c
'e0d8a816b021eaf5c36d101892180f79df655c5712b348c2a540ca136e6b22001') _has_aria2c: bool = False
_X_PUBLIC_KEY = ('04d9d2319e0480c840efeeb75751b86d0db0c5b9e72c6260a1d846958adceaf9d' # aligo
'ee789cab7472741d23aafc1a9c591f72e7ee77578656e6c8588098dea1488ac2a') aligo: Aligo = None
# 生成二维码 # 生成二维码
qrcode_url = ("https://passport.aliyundrive.com/newlogin/qrcode/generate.do?" 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") "&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" 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): def __init__(self):
""" super().__init__()
统一处理和打印错误信息
"""
if res is None:
logger.warn("无法连接到阿里云盘!")
return
try: try:
result = res.json() subprocess.run(['aria2c', '-h'], capture_output=True)
except Exception as err: self._has_aria2c = True
logger.error(f"解析阿里云盘返回数据失败:{str(err)}") logger.debug('发现 aria2c, 将使用 aria2c 下载文件')
return except FileNotFoundError:
code = result.get("code") logger.debug('未发现 aria2c')
message = result.get("message") self._has_aria2c = False
display_message = result.get("display_message")
if code or message: self.__init_aligo()
logger.warn(f"Aliyun {apiname}失败:{code} - {display_message or message}")
if action: def __init_aligo(self):
if code == "DeviceSessionSignatureInvalid": """
logger.warn("设备已失效,正在重新建立会话...") 初始化 aligo
self.__create_session(self.__get_headers(self.__auth_params)) """
if code == "UserDeviceOffline": refresh_token = self.__auth_params.get("refreshToken")
logger.warn("设备已离线,尝试重新登录,如仍报错请检查阿里云盘绑定设备数量是否超限!") if refresh_token:
self.__create_session(self.__get_headers(self.__auth_params)) self.aligo = Aligo(refresh_token=refresh_token, use_aria2=self._has_aria2c,
if code == "AccessTokenInvalid": name="MoviePilot V2", level=logging.ERROR)
logger.warn("访问令牌已失效,正在刷新令牌...")
self.__update_accesstoken(self.__auth_params, self.__auth_params.get("refreshToken"))
else:
logger.info(f"Aliyun {apiname}成功")
@property @property
def __auth_params(self): def __auth_params(self):
@@ -135,7 +99,6 @@ class AliPan(StorageBase):
"t": data.get("t") "t": data.get("t")
}, "" }, ""
elif res is not None: elif res is not None:
self.__handle_error(res, "生成二维码")
return {}, f"请求阿里云盘二维码失败:{res.status_code} - {res.reason}" return {}, f"请求阿里云盘二维码失败:{res.status_code} - {res.reason}"
return {}, f"请求阿里云盘二维码失败:无法连接!" return {}, f"请求阿里云盘二维码失败:无法连接!"
@@ -194,151 +157,62 @@ class AliPan(StorageBase):
"updateTime": time.time(), "updateTime": time.time(),
}) })
self.__update_params(data) self.__update_params(data)
self.user_info() self.__init_aligo()
except Exception as e: except Exception as e:
return {}, f"bizExt 解码失败:{str(e)}" return {}, f"bizExt 解码失败:{str(e)}"
return data, "" return data, ""
elif res is not None: elif res is not None:
self.__handle_error(res, "登录确认")
return {}, f"阿里云盘登录确认失败:{res.status_code} - {res.reason}" return {}, f"阿里云盘登录确认失败:{res.status_code} - {res.reason}"
return {}, "阿里云盘登录确认失败:无法连接!" 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: def check(self) -> bool:
""" """
检查存储是否可用 检查存储是否可用
""" """
params = self.__access_params if not self.aligo:
if not params:
return False return False
return True if self.list(schemas.FileItem( return True if self.aligo.get_user() else False
fileid="root",
drive_id=params.get("resourceDriveId")
)) else False
def user_info(self) -> dict: def user_info(self) -> dict:
""" """
获取用户信息drive_id等 获取用户信息drive_id等
""" """
params = self.__access_params return self.aligo.get_user()
if not params:
return {} def __get_fileitem(self, fileinfo: BaseFile, parent: str = "/") -> schemas.FileItem:
headers = self.__get_headers(params) """
res = RequestUtils(headers=headers, timeout=10).post_res(self.user_info_url) 获取文件信息
if res: """
result = res.json() if not fileinfo:
self.__update_params({ return schemas.FileItem()
"resourceDriveId": result.get("resource_drive_id"), if fileinfo.type == "folder":
"backDriveId": result.get("backup_drive_id") return schemas.FileItem(
}) storage=self.schema.value,
return result 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: else:
self.__handle_error(res, "获取用户信息") return schemas.FileItem(
return {} 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]: def list(self, fileitem: schemas.FileItem = None) -> List[schemas.FileItem]:
""" """
@@ -348,18 +222,15 @@ class AliPan(StorageBase):
parent_file_id 根目录为root parent_file_id 根目录为root
type all | file | folder type all | file | folder
""" """
params = self.__access_params if not self.aligo:
if not params:
return [] return []
# 请求头
headers = self.__get_headers(params)
# 根目录处理 # 根目录处理
if not fileitem or not fileitem.drive_id: if not fileitem or not fileitem.drive_id:
return [ return [
schemas.FileItem( schemas.FileItem(
storage=self.schema.value, storage=self.schema.value,
fileid=fileitem.fileid, fileid=fileitem.fileid,
drive_id=params.get("resourceDriveId"), drive_id=self.__auth_params.get("resourceDriveId"),
parent_fileid="root", parent_fileid="root",
type="dir", type="dir",
path="/资源库/", path="/资源库/",
@@ -369,7 +240,7 @@ class AliPan(StorageBase):
schemas.FileItem( schemas.FileItem(
storage=self.schema.value, storage=self.schema.value,
fileid=fileitem.fileid, fileid=fileitem.fileid,
drive_id=params.get("backDriveId"), drive_id=self.__auth_params.get("backDriveId"),
parent_fileid="root", parent_fileid="root",
type="dir", type="dir",
path="/备份盘/", path="/备份盘/",
@@ -377,436 +248,126 @@ class AliPan(StorageBase):
basename="备份盘" basename="备份盘"
) )
] ]
# 如果本身是文件 elif fileitem.type == "file":
if fileitem.type == "file": # 文件处理
return [fileitem] file = self.detail(fileitem)
# 返回数据 if file:
ret_items = [] return [file]
# 分页获取 else:
next_marker = None items = self.aligo.get_file_list(parent_file_id=fileitem.fileid, drive_id=fileitem.drive_id)
while True: if items:
if not fileitem.parent_fileid or fileitem.parent_fileid == "/": return [self.__get_fileitem(item) for item in items]
parent_file_id = "root" return []
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]
def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]: def create_folder(self, fileitem: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
""" """
创建目录 创建目录
""" """
params = self.__access_params if not self.aligo:
if not params:
return None return None
headers = self.__get_headers(params) item = self.aligo.create_folder(name=name, parent_file_id=fileitem.fileid, drive_id=fileitem.drive_id)
res = RequestUtils(headers=headers, timeout=10).post_res(self.create_folder_file_url, json={ if item:
"drive_id": fileitem.drive_id, return self.__get_fileitem(item)
"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, "创建目录")
return None return None
def get_folder(self, path: Path) -> Optional[schemas.FileItem]: def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
""" """
根据文件路程获取目录,不存在则创建 根据文件路程获取目录,不存在则创建
""" """
if not self.aligo:
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
return None return None
item = self.aligo.get_folder_by_path(path=str(Path), create_folder=True)
# 逐级查找和创建目录 if item:
fileitem = schemas.FileItem(fileid="root") return self.__get_fileitem(item)
for part in path.parts: return None
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
def get_item(self, path: Path) -> Optional[schemas.FileItem]: def get_item(self, path: Path) -> Optional[schemas.FileItem]:
""" """
获取文件或目录不存在返回None 获取文件或目录不存在返回None
""" """
if not self.aligo:
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
return None return None
item = self.aligo.get_file_by_path(path=str(path))
# 逐级查找和创建目录 if item:
fileitem = schemas.FileItem(fileid="root") return self.__get_fileitem(item)
for part in path.parts: return None
if part == "/":
continue
item = __find_item(fileitem, part)
if not item:
return None
fileitem = item
return fileitem
def delete(self, fileitem: schemas.FileItem) -> bool: def delete(self, fileitem: schemas.FileItem) -> bool:
""" """
删除文件 删除文件
""" """
params = self.__access_params if not self.aligo:
if not params:
return False return False
headers = self.__get_headers(params) if self.aligo.move_file_to_trash(file_id=fileitem.fileid, drive_id=fileitem.drive_id):
res = RequestUtils(headers=headers, timeout=10).post_res(self.delete_file_url, json={
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid
})
if res:
return True return True
else:
self.__handle_error(res, "删除文件")
return False return False
def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]: def detail(self, fileitem: schemas.FileItem) -> Optional[schemas.FileItem]:
""" """
获取文件详情 获取文件详情
""" """
params = self.__access_params if not self.aligo:
if not params:
return None return None
headers = self.__get_headers(params) item = self.aligo.get_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id)
res = RequestUtils(headers=headers, timeout=10).post_res(self.file_detail_url, json={ if item:
"drive_id": fileitem.drive_id, return self.__get_fileitem(item)
"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, "获取文件详情")
return None return None
def rename(self, fileitem: schemas.FileItem, name: str) -> bool: def rename(self, fileitem: schemas.FileItem, name: str) -> bool:
""" """
重命名文件 重命名文件
""" """
params = self.__access_params if not self.aligo:
if not params:
return False return False
headers = self.__get_headers(params) if self.aligo.rename_file(file_id=fileitem.fileid, name=name, drive_id=fileitem.drive_id):
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:
return True return True
else:
self.__handle_error(res, "重命名文件")
return False 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 self.aligo:
if not params:
return None return None
headers = self.__get_headers(params) local_path = self.aligo.download_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id,
local_folder=str(path or settings.TEMP_PATH))
def __get_play_url(): if local_path:
""" return Path(local_path)
获取播放地址
"""
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, "获取下载链接")
return None return None
def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]: def upload(self, fileitem: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
""" """
上传文件,并标记完成 上传文件,并标记完成
""" """
if not self.aligo:
__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:
return None return None
headers = self.__get_headers(params) item = self.aligo.upload_file(file_path=str(path.parent), parent_file_id=fileitem.fileid,
drive_id=fileitem.drive_id, name=path.name)
# 计算sha1 if item:
sha1 = __sha1(path) return self.__get_fileitem(item)
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("阿里云盘上传文件失败:无法获取上传地址!")
return None return None
def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool: def move(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool:
""" """
移动文件 移动文件
""" """
params = self.__access_params if not self.aligo:
if not params:
return False return False
headers = self.__get_headers(params) if self.aligo.move_file(file_id=fileitem.fileid, drive_id=fileitem.drive_id,
res = RequestUtils(headers=headers, timeout=10).post_res(self.move_file_url, json={ to_parent_file_id=target.fileid, to_drive_id=target.drive_id):
"drive_id": fileitem.drive_id,
"file_id": fileitem.fileid,
"to_parent_file_id": target.fileid,
"check_name_mode": "refuse"
})
if res:
return True return True
else:
self.__handle_error(res, "移动文件")
return False return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def copy(self, fileitem: schemas.FileItem, target: schemas.FileItem) -> bool:
"""
复制文件
"""
pass pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
""" """
硬链接文件 硬链接文件
""" """
pass 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 self.aligo:
if not params:
return None return None
headers = self.__get_headers(params) user_capacity = self.aligo.get_user_capacity_info()
res = RequestUtils(headers=headers, timeout=10).post_res(self.storage_info_url, json={}) if user_capacity:
if res: drive_capacity = user_capacity.drive_capacity_details
result = res.json() if drive_capacity:
return schemas.StorageUsage( return schemas.StorageUsage(
total=result.get("drive_total_size"), total=drive_capacity.drive_total_size,
available=result.get("drive_total_size") - result.get("drive_used_size") available=drive_capacity.drive_total_size - drive_capacity.drive_used_size
) )
else:
self.__handle_error(res, "查询存储详情")
return None return None

View File

@@ -135,11 +135,11 @@ class LocalStorage(StorageBase):
return self.__get_fileitem(path) return self.__get_fileitem(path)
return self.__get_diritem(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(): if not path_obj.exists():
return None return None
return self.__get_fileitem(path_obj) return self.__get_fileitem(path_obj)
@@ -177,7 +177,7 @@ class LocalStorage(StorageBase):
return False return False
return True 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}") logger.error(f"rclone浏览文件失败{err}")
return [] 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( retcode = subprocess.run(
[ [
'rclone', 'mkdir', 'rclone', 'mkdir',
f'MP:{fileitm.path}/{name}' f'MP:{fileitem.path}/{name}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
if retcode == 0: if retcode == 0:
ret_fileitem = copy.deepcopy(fileitm) ret_fileitem = copy.deepcopy(fileitem)
ret_fileitem.path = f"{fileitm.path}/{name}/" ret_fileitem.path = f"{fileitem.path}/{name}/"
ret_fileitem.name = name ret_fileitem.name = name
return ret_fileitem return ret_fileitem
except Exception as err: except Exception as err:
@@ -191,7 +191,7 @@ class Rclone(StorageBase):
logger.error(f"rclone获取文件失败{err}") logger.error(f"rclone获取文件失败{err}")
return None 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( retcode = subprocess.run(
[ [
'rclone', 'deletefile', 'rclone', 'deletefile',
f'MP:{fileitm.path}' f'MP:{fileitem.path}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
@@ -209,7 +209,7 @@ class Rclone(StorageBase):
logger.error(f"rclone删除文件失败{err}") logger.error(f"rclone删除文件失败{err}")
return False 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( retcode = subprocess.run(
[ [
'rclone', 'moveto', 'rclone', 'moveto',
f'MP:{fileitm.path}', f'MP:{fileitem.path}',
f'MP:{Path(fileitm.path).parent}/{name}' f'MP:{Path(fileitem.path).parent}/{name}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
).returncode ).returncode
@@ -228,11 +228,11 @@ class Rclone(StorageBase):
logger.error(f"rclone重命名文件失败{err}") logger.error(f"rclone重命名文件失败{err}")
return False 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: try:
retcode = subprocess.run( retcode = subprocess.run(
[ [
@@ -248,7 +248,7 @@ class Rclone(StorageBase):
logger.error(f"rclone复制文件失败{err}") logger.error(f"rclone复制文件失败{err}")
return None 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( retcode = subprocess.run(
[ [
'rclone', 'copyto', 'rclone', 'copyto',
fileitm.path, fileitem.path,
f'MP:{path}' f'MP:{path}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
@@ -267,7 +267,7 @@ class Rclone(StorageBase):
logger.error(f"rclone上传文件失败{err}") logger.error(f"rclone上传文件失败{err}")
return None 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( ret = subprocess.run(
[ [
'rclone', 'lsjson', 'rclone', 'lsjson',
f'MP:{fileitm.path}' f'MP:{fileitem.path}'
], ],
capture_output=True, capture_output=True,
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
@@ -287,7 +287,7 @@ class Rclone(StorageBase):
logger.error(f"rclone获取文件详情失败{err}") logger.error(f"rclone获取文件详情失败{err}")
return None return None
def move(self, fileitm: schemas.FileItem, target: Path) -> bool: def move(self, fileitem: schemas.FileItem, target: Path) -> bool:
""" """
移动文件target_file格式rclone:path 移动文件target_file格式rclone:path
""" """
@@ -295,7 +295,7 @@ class Rclone(StorageBase):
retcode = subprocess.run( retcode = subprocess.run(
[ [
'rclone', 'moveto', 'rclone', 'moveto',
f'MP:{fileitm.path}', f'MP:{fileitem.path}',
f'MP:{target}' f'MP:{target}'
], ],
startupinfo=self.__get_hidden_shell() startupinfo=self.__get_hidden_shell()
@@ -306,13 +306,13 @@ class Rclone(StorageBase):
logger.error(f"rclone移动文件失败{err}") logger.error(f"rclone移动文件失败{err}")
return False return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def usage(self) -> Optional[schemas.StorageUsage]: def usage(self) -> Optional[schemas.StorageUsage]:

View File

@@ -1,4 +1,5 @@
import base64 import base64
import subprocess
from pathlib import Path from pathlib import Path
from typing import Optional, Tuple, List from typing import Optional, Tuple, List
@@ -32,6 +33,19 @@ class U115Pan(StorageBase, metaclass=Singleton):
cloud: Optional[Cloud] = None cloud: Optional[Cloud] = None
_session: QrcodeSession = 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: def __init_cloud(self) -> bool:
""" """
初始化Cloud 初始化Cloud
@@ -260,7 +274,7 @@ class U115Pan(StorageBase, metaclass=Singleton):
fileitem = item fileitem = item
return fileitem 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)}") logger.error(f"115重命名文件失败{str(e)}")
return False 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: try:
ticket = self.cloud.storage().request_download(fileitem.pickcode) ticket = self.cloud.storage().request_download(fileitem.pickcode)
if ticket: 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) res = RequestUtils(headers=ticket.headers).get_res(ticket.url)
if res: if res:
with open(path, "wb") as f: with open(path, "wb") as f:
@@ -374,13 +388,13 @@ class U115Pan(StorageBase, metaclass=Singleton):
logger.error(f"115移动文件失败{str(e)}") logger.error(f"115移动文件失败{str(e)}")
return False return False
def copy(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def copy(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def link(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def link(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def softlink(self, fileitm: schemas.FileItem, target_file: Path) -> bool: def softlink(self, fileitem: schemas.FileItem, target_file: Path) -> bool:
pass pass
def usage(self) -> Optional[schemas.StorageUsage]: def usage(self) -> Optional[schemas.StorageUsage]: