mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-07 08:26:53 +08:00
fix rclone
This commit is contained in:
@@ -56,31 +56,32 @@ class FileManagerModule(_ModuleBase):
|
|||||||
if not dirs:
|
if not dirs:
|
||||||
return False, "未设置任何目录"
|
return False, "未设置任何目录"
|
||||||
for d in dirs:
|
for d in dirs:
|
||||||
|
# 下载目录
|
||||||
download_path = d.download_path
|
download_path = d.download_path
|
||||||
if not download_path:
|
if not download_path:
|
||||||
return False, f"{d.name} 的下载目录未设置"
|
return False, f"{d.name} 的下载目录未设置"
|
||||||
if d.storage == "local" and not Path(download_path).exists():
|
if d.storage == "local" and not Path(download_path).exists():
|
||||||
return False, f"{d.name} 的下载目录 {download_path} 不存在"
|
return False, f"{d.name} 的下载目录 {download_path} 不存在"
|
||||||
|
# 媒体库目录
|
||||||
library_path = d.library_path
|
library_path = d.library_path
|
||||||
if not library_path:
|
if not library_path:
|
||||||
return False, f"{d.name} 的媒体库目录未设置"
|
return False, f"{d.name} 的媒体库目录未设置"
|
||||||
if d.library_storage == "local" and not Path(library_path).exists():
|
if d.library_storage == "local" and not Path(library_path).exists():
|
||||||
return False, f"{d.name} 的媒体库目录 {library_path} 不存在"
|
return False, f"{d.name} 的媒体库目录 {library_path} 不存在"
|
||||||
# 检查软硬链接
|
# 硬链接
|
||||||
if d.transfer_type in ["link", "softlink"] \
|
|
||||||
and (d.storage != "local" or d.library_storage != "local"):
|
|
||||||
return False, f"{d.name} 不是本地存储,不支持软硬链接"
|
|
||||||
# 检查硬链接
|
|
||||||
if d.transfer_type == "link" \
|
if d.transfer_type == "link" \
|
||||||
|
and d.storage == "local" \
|
||||||
|
and d.library_storage == "local" \
|
||||||
and not SystemUtils.is_same_disk(Path(download_path), Path(library_path)):
|
and not SystemUtils.is_same_disk(Path(download_path), Path(library_path)):
|
||||||
return False, f"{d.name} 的下载目录 {download_path} 与媒体库目录 {library_path} 不在同一磁盘,无法硬链接"
|
return False, f"{d.name} 的下载目录 {download_path} 与媒体库目录 {library_path} 不在同一磁盘,无法硬链接"
|
||||||
# 检查网盘
|
# 存储
|
||||||
if d.storage != "local":
|
storage_oper = self.__get_storage_oper(d.storage)
|
||||||
storage_oper = self.__get_storage_oper(d.storage)
|
if not storage_oper:
|
||||||
if not storage_oper:
|
return False, f"{d.name} 的存储类型 {d.storage} 不支持"
|
||||||
return False, f"{d.name} 的存储类型 {d.storage} 不支持"
|
if not storage_oper.check():
|
||||||
if not storage_oper.check():
|
return False, f"{d.name} 的存储测试不通过"
|
||||||
return False, f"{d.name} 的存储测试不通过"
|
if d.transfer_type and d.transfer_type not in storage_oper.support_transtype():
|
||||||
|
return False, f"{d.name} 的存储不支持 {d.transfer_type} 整理方式"
|
||||||
|
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class StorageBase(metaclass=ABCMeta):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def download(self, fileitm: schemas.FileItem, path: Path):
|
def download(self, fileitm: schemas.FileItem, path: Path) -> bool:
|
||||||
"""
|
"""
|
||||||
下载文件,保存到本地
|
下载文件,保存到本地
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import copy
|
||||||
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
@@ -6,6 +8,7 @@ from app import schemas
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules.filemanager.storage import StorageBase
|
from app.modules.filemanager.storage import StorageBase
|
||||||
from app.schemas.types import StorageSchema
|
from app.schemas.types import StorageSchema
|
||||||
|
from app.utils.string import StringUtils
|
||||||
from app.utils.system import SystemUtils
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -16,6 +19,7 @@ class Rclone(StorageBase):
|
|||||||
|
|
||||||
# 存储类型
|
# 存储类型
|
||||||
schema = StorageSchema.Rclone
|
schema = StorageSchema.Rclone
|
||||||
|
|
||||||
# 支持的整理方式
|
# 支持的整理方式
|
||||||
transtype = {
|
transtype = {
|
||||||
"move": "移动",
|
"move": "移动",
|
||||||
@@ -32,37 +36,221 @@ class Rclone(StorageBase):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def __get_fileitem(self, path: Path):
|
||||||
|
"""
|
||||||
|
获取文件项
|
||||||
|
"""
|
||||||
|
return schemas.FileItem(
|
||||||
|
storage=self.schema.value,
|
||||||
|
type="file",
|
||||||
|
path=str(path).replace("\\", "/"),
|
||||||
|
name=path.name,
|
||||||
|
basename=path.stem,
|
||||||
|
extension=path.suffix[1:],
|
||||||
|
size=path.stat().st_size,
|
||||||
|
modify_time=path.stat().st_mtime,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __get_rcloneitem(self, item: dict):
|
||||||
|
"""
|
||||||
|
获取rclone文件项
|
||||||
|
"""
|
||||||
|
return schemas.FileItem(
|
||||||
|
storage=self.schema.value,
|
||||||
|
type="dir" if item.get("IsDir") else "file",
|
||||||
|
path=item.get("Path"),
|
||||||
|
name=item.get("Name"),
|
||||||
|
basename=Path(item.get("Name")).stem,
|
||||||
|
extension=Path(item.get("Name")).suffix[1:],
|
||||||
|
size=item.get("Size"),
|
||||||
|
modify_time=StringUtils.str_to_timestamp(item.get("ModTime"))
|
||||||
|
)
|
||||||
|
|
||||||
def check(self) -> bool:
|
def check(self) -> bool:
|
||||||
pass
|
"""
|
||||||
|
检查存储是否可用
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
['rclone', 'lsf', 'MP:'],
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
if retcode == 0:
|
||||||
|
return True
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"rclone存储检查失败:{err}")
|
||||||
|
return False
|
||||||
|
|
||||||
def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
|
def list(self, fileitm: schemas.FileItem) -> Optional[List[schemas.FileItem]]:
|
||||||
pass
|
"""
|
||||||
|
浏览文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'lsjson',
|
||||||
|
f'MP:{fileitm.path}'
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
)
|
||||||
|
if ret.returncode == 0:
|
||||||
|
items = json.loads(ret.stdout)
|
||||||
|
return [self.__get_rcloneitem(item) for item in items]
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"浏览文件失败:{err}")
|
||||||
|
return None
|
||||||
|
|
||||||
def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
def create_folder(self, fileitm: schemas.FileItem, name: str) -> Optional[schemas.FileItem]:
|
||||||
pass
|
"""
|
||||||
|
创建目录
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'mkdir',
|
||||||
|
f'MP:{fileitm.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.name = name
|
||||||
|
return ret_fileitem
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"创建目录失败:{err}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
def get_folder(self, path: Path) -> Optional[schemas.FileItem]:
|
||||||
"""
|
"""
|
||||||
获取目录
|
根据文件路程获取目录,不存在则创建
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
def __find_dir_name(_fileitem: schemas.FileItem, _name: str) -> Optional[schemas.FileItem]:
|
||||||
|
"""
|
||||||
|
查找下级目录中匹配名称的目录
|
||||||
|
"""
|
||||||
|
sub_files = self.list(_fileitem)
|
||||||
|
for sub_file in sub_files:
|
||||||
|
if sub_file.type != "dir":
|
||||||
|
continue
|
||||||
|
if sub_file.name == _name:
|
||||||
|
return sub_file
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 逐级查找和创建目录
|
||||||
|
fileitem = schemas.FileItem(path="/")
|
||||||
|
for part in path.parts:
|
||||||
|
if part == "/":
|
||||||
|
continue
|
||||||
|
dir_file = __find_dir_name(fileitem, part)
|
||||||
|
if dir_file:
|
||||||
|
return dir_file
|
||||||
|
else:
|
||||||
|
dir_file = self.create_folder(dir_file, part)
|
||||||
|
if not dir_file:
|
||||||
|
logger.warn(f"rclone创建目录 {fileitem.path}{part} 失败!")
|
||||||
|
return None
|
||||||
|
fileitem = dir_file
|
||||||
|
return None
|
||||||
|
|
||||||
def delete(self, fileitm: schemas.FileItem) -> bool:
|
def delete(self, fileitm: schemas.FileItem) -> bool:
|
||||||
pass
|
"""
|
||||||
|
删除文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'deletefile',
|
||||||
|
f'MP:{fileitm.path}'
|
||||||
|
],
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
if retcode == 0:
|
||||||
|
return True
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"删除文件失败:{err}")
|
||||||
|
return False
|
||||||
|
|
||||||
def rename(self, fileitm: schemas.FileItem, name: str) -> bool:
|
def rename(self, fileitm: schemas.FileItem, name: str) -> bool:
|
||||||
pass
|
"""
|
||||||
|
重命名文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'moveto',
|
||||||
|
f'MP:{fileitm.path}',
|
||||||
|
f'MP:{Path(fileitm.path).parent}/{name}'
|
||||||
|
],
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
if retcode == 0:
|
||||||
|
return True
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"重命名文件失败:{err}")
|
||||||
|
return False
|
||||||
|
|
||||||
def download(self, fileitm: schemas.FileItem, path: Path):
|
def download(self, fileitm: schemas.FileItem, path: Path) -> bool:
|
||||||
pass
|
"""
|
||||||
|
下载文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'copyto',
|
||||||
|
f'MP:{fileitm.path}',
|
||||||
|
f'{path}'
|
||||||
|
],
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
if retcode == 0:
|
||||||
|
return True
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"复制文件失败:{err}")
|
||||||
|
return False
|
||||||
|
|
||||||
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
def upload(self, fileitm: schemas.FileItem, path: Path) -> Optional[schemas.FileItem]:
|
||||||
pass
|
"""
|
||||||
|
上传文件
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
retcode = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'copyto',
|
||||||
|
fileitm.path,
|
||||||
|
f'MP:{path}'
|
||||||
|
],
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
).returncode
|
||||||
|
if retcode == 0:
|
||||||
|
return self.__get_fileitem(path)
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"上传文件失败:{err}")
|
||||||
|
return None
|
||||||
|
|
||||||
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
|
def detail(self, fileitm: schemas.FileItem) -> Optional[schemas.FileItem]:
|
||||||
pass
|
"""
|
||||||
|
获取文件详情
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = subprocess.run(
|
||||||
|
[
|
||||||
|
'rclone', 'lsjson',
|
||||||
|
f'MP:{fileitm.path}'
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
startupinfo=self.__get_hidden_shell()
|
||||||
|
)
|
||||||
|
if ret.returncode == 0:
|
||||||
|
items = json.loads(ret.stdout)
|
||||||
|
return self.__get_rcloneitem(items[0])
|
||||||
|
except Exception as err:
|
||||||
|
logger.error(f"获取文件详情失败:{err}")
|
||||||
|
return None
|
||||||
|
|
||||||
def move(self, fileitm: schemas.FileItem, target_file: schemas.FileItem) -> bool:
|
def move(self, fileitm: schemas.FileItem, target_file: Path) -> bool:
|
||||||
"""
|
"""
|
||||||
移动文件,target_file格式:rclone:path
|
移动文件,target_file格式:rclone:path
|
||||||
"""
|
"""
|
||||||
@@ -70,8 +258,8 @@ class Rclone(StorageBase):
|
|||||||
retcode = subprocess.run(
|
retcode = subprocess.run(
|
||||||
[
|
[
|
||||||
'rclone', 'moveto',
|
'rclone', 'moveto',
|
||||||
fileitm.path,
|
f'MP:{fileitm.path}',
|
||||||
f'{target_file}'
|
f'MP:{target_file}'
|
||||||
],
|
],
|
||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
).returncode
|
).returncode
|
||||||
@@ -89,8 +277,8 @@ class Rclone(StorageBase):
|
|||||||
retcode = subprocess.run(
|
retcode = subprocess.run(
|
||||||
[
|
[
|
||||||
'rclone', 'copyto',
|
'rclone', 'copyto',
|
||||||
fileitm.path,
|
f'MP:{fileitm.path}',
|
||||||
f'{target_file}'
|
f'MP:{target_file}'
|
||||||
],
|
],
|
||||||
startupinfo=self.__get_hidden_shell()
|
startupinfo=self.__get_hidden_shell()
|
||||||
).returncode
|
).returncode
|
||||||
|
|||||||
Reference in New Issue
Block a user