@@ -56,11 +56,12 @@ def require_param(self, case: Any, msg: str):
5656
5757 raise PluginValidationException (self , msg )
5858
59- def warning_lib_not_install (self , lib : str ):
59+ def warning_lib_not_install (self , lib : str , throw = False ):
6060 msg = (f'插件`{ self .plugin_key } `依赖库: { lib } ,请先安装{ lib } 再使用。'
6161 f'安装命令: [pip install { lib } ]' )
6262 import warnings
6363 warnings .warn (msg )
64+ self .require_param (throw , msg )
6465
6566 def execute_deletion (self , paths : List [str ]):
6667 """
@@ -303,6 +304,11 @@ def do_filter(self, detail):
303304
304305
305306class ZipPlugin (JmOptionPlugin ):
307+ """
308+ 感谢zip加密功能的贡献者:
309+ - AXIS5 a.k.a AXIS5Hacker (https://github.com/hect0x7/JMComic-Crawler-Python/pull/375)
310+ """
311+
306312 plugin_key = 'zip'
307313
308314 # noinspection PyAttributeOutsideInit
@@ -316,6 +322,7 @@ def invoke(self,
316322 suffix = 'zip' ,
317323 zip_dir = './' ,
318324 dir_rule = None ,
325+ encrypt = None ,
319326 ) -> None :
320327
321328 from .jm_downloader import JmDownloader
@@ -333,35 +340,39 @@ def invoke(self,
333340
334341 if level == 'album' :
335342 zip_path = self .decide_filepath (album , None , filename_rule , suffix , zip_dir , dir_rule )
336- self .zip_album (album , photo_dict , zip_path , path_to_delete )
343+ self .zip_album (album , photo_dict , zip_path , path_to_delete , encrypt )
337344
338345 elif level == 'photo' :
339346 for photo , image_list in photo_dict .items ():
340347 zip_path = self .decide_filepath (photo .from_album , photo , filename_rule , suffix , zip_dir , dir_rule )
341- self .zip_photo (photo , image_list , zip_path , path_to_delete )
348+ self .zip_photo (photo , image_list , zip_path , path_to_delete , encrypt )
342349
343350 else :
344351 ExceptionTool .raises (f'Not Implemented Zip Level: { level } ' )
345352
346353 self .after_zip (path_to_delete )
347354
355+ # noinspection PyMethodMayBeStatic
348356 def get_downloaded_photo (self , downloader , album , photo ):
349357 return (
350358 downloader .download_success_dict [album ]
351359 if album is not None # after_album
352360 else downloader .download_success_dict [photo .from_album ] # after_photo
353361 )
354362
355- def zip_photo (self , photo , image_list : list , zip_path : str , path_to_delete ):
363+ def zip_photo (self , photo , image_list : list , zip_path : str , path_to_delete , encrypt_dict ):
356364 """
357365 压缩photo文件夹
358366 """
359367 photo_dir = self .option .decide_image_save_dir (photo ) \
360368 if len (image_list ) == 0 \
361369 else os .path .dirname (image_list [0 ][0 ])
362370
363- from common import backup_dir_to_zip
364- backup_dir_to_zip (photo_dir , zip_path )
371+ with self .open_zip_file (zip_path , encrypt_dict ) as f :
372+ for file in files_of_dir (photo_dir ):
373+ abspath = os .path .join (photo_dir , file )
374+ relpath = os .path .relpath (abspath , photo_dir )
375+ f .write (abspath , relpath )
365376
366377 self .log (f'压缩章节[{ photo .photo_id } ]成功 → { zip_path } ' , 'finish' )
367378 path_to_delete .append (self .unified_path (photo_dir ))
@@ -370,14 +381,13 @@ def zip_photo(self, photo, image_list: list, zip_path: str, path_to_delete):
370381 def unified_path (f ):
371382 return fix_filepath (f , os .path .isdir (f ))
372383
373- def zip_album (self , album , photo_dict : dict , zip_path , path_to_delete ):
384+ def zip_album (self , album , photo_dict : dict , zip_path , path_to_delete , encrypt_dict ):
374385 """
375386 压缩album文件夹
376387 """
377388
378389 album_dir = self .option .dir_rule .decide_album_root_dir (album )
379- import zipfile
380- with zipfile .ZipFile (zip_path , 'w' , zipfile .ZIP_DEFLATED ) as f :
390+ with self .open_zip_file (zip_path , encrypt_dict ) as f :
381391 for photo in photo_dict .keys ():
382392 # 定位到章节所在文件夹
383393 photo_dir = self .unified_path (self .option .decide_image_save_dir (photo ))
@@ -401,6 +411,67 @@ def after_zip(self, path_to_delete: List[str]):
401411 self .execute_deletion (image_paths )
402412 self .execute_deletion (dirs )
403413
414+ # noinspection PyMethodMayBeStatic
415+ @classmethod
416+ def generate_random_str (cls , random_length ) -> str :
417+ """
418+ 自动生成随机字符密码,长度由randomlength指定
419+ """
420+ import random
421+
422+ random_str = ''
423+ base_str = r'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
424+ base_length = len (base_str ) - 1
425+ for _ in range (random_length ):
426+ random_str += base_str [random .randint (0 , base_length )]
427+ return random_str
428+
429+ def open_zip_file (self , zip_path : str , encrypt_dict : Optional [dict ]):
430+ if encrypt_dict is None :
431+ import zipfile
432+ return zipfile .ZipFile (zip_path , 'w' , zipfile .ZIP_DEFLATED )
433+
434+ password , is_random = self .decide_password (encrypt_dict , zip_path )
435+ if encrypt_dict .get ('impl' , '' ) == '7z' :
436+ try :
437+ # noinspection PyUnresolvedReferences
438+ import py7zr
439+ except ImportError :
440+ self .warning_lib_not_install ('py7zr' )
441+
442+ # noinspection PyUnboundLocalVariable
443+ filters = [{'id' : py7zr .FILTER_COPY }]
444+ return py7zr .SevenZipFile (zip_path , mode = 'w' , password = password , filters = filters , header_encryption = True )
445+ else :
446+ try :
447+ # noinspection PyUnresolvedReferences
448+ import pyzipper
449+ except ImportError :
450+ self .warning_lib_not_install ('pyzipper' )
451+
452+ # noinspection PyUnboundLocalVariable
453+ aes_zip_file = pyzipper .AESZipFile (zip_path , "w" , pyzipper .ZIP_DEFLATED )
454+ aes_zip_file .setencryption (pyzipper .WZ_AES , nbits = 128 )
455+ password_bytes = str .encode (password )
456+ aes_zip_file .setpassword (password_bytes )
457+ if is_random :
458+ aes_zip_file .comment = password_bytes
459+ return aes_zip_file
460+
461+ def decide_password (self , encrypt_dict : dict , zip_path : str ):
462+ encrypt_type = encrypt_dict .get ('type' , '' )
463+ is_random = False
464+
465+ if encrypt_type == 'random' :
466+ is_random = True
467+ password = self .generate_random_str (48 )
468+ self .log (f'生成随机密码: [{ password } ] → [{ zip_path } ]' , 'encrypt' )
469+ else :
470+ password = str (encrypt_dict ['password' ])
471+ self .log (f'使用指定密码: [{ password } ] → [{ zip_path } ]' , 'encrypt' )
472+
473+ return password , is_random
474+
404475
405476class ClientProxyPlugin (JmOptionPlugin ):
406477 plugin_key = 'client_proxy'
@@ -869,6 +940,7 @@ def invoke(self,
869940 # 服务器的代码位于一个独立库:plugin_jm_server,需要独立安装
870941 # 源代码仓库:https://github.com/hect0x7/plugin-jm-server
871942 try :
943+ # noinspection PyUnresolvedReferences
872944 import plugin_jm_server
873945 self .log (f'当前使用plugin_jm_server版本: { plugin_jm_server .__version__ } ' )
874946 except ImportError :
0 commit comments