Coverage for seedboxsync/core/sync/services/seedbox.py: 89%

89 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-26 17:46 +0000

1# 

2# Copyright (C) 2015-2026 Guillaume Kulakowski <guillaume@kulakowski.fr> 

3# 

4# For the full copyright and license information, please view the LICENSE 

5# file that was distributed with this source code. 

6# 

7"""SeedboxSync sync service for seedbox.""" 

8 

9import datetime 

10from os import PathLike, fspath 

11from pathlib import Path 

12import re 

13from paramiko import SSHException 

14from seedboxsync.core import current_app as app, utils 

15from seedboxsync.core.dao import Download 

16from seedboxsync.core.exception import SeedboxSyncConfigurationError 

17from seedboxsync.core.sync.download_progress import DownloadProgress 

18from seedboxsync.core.taskmanager import track_taskstatus 

19 

20LOCK_NAME = "sync-seedbox" 

21PRIORITY = 5 

22 

23 

24@track_taskstatus(LOCK_NAME) 

25def seedbox(dry_run: bool, ping: bool, only_store: bool) -> None: 

26 """ 

27 Perform synchronization from the seedbox. 

28 

29 Downloads files from the remote seedbox to the local machine, 

30 supports optional dry-run and only-store modes, applies exclusion patterns, 

31 and persists download information in the database. 

32 

33 Args: 

34 dry_run (bool): Whether to list files without downloading or persisting them. 

35 ping (bool): Whether to ping the configured monitoring service. 

36 only_store (bool): Whether to record remote files without downloading them. 

37 """ 

38 if not app.seedboxsync_config.get("sync_seedbox_enabled"): 

39 app.logger.info("Seedbox synchronization task is disabled") 

40 return 

41 

42 app.logger.debug(f'sync seedbox dry-run: "{dry_run}"') 

43 app.logger.debug(f'sync seeddbox only-store: "{only_store}"') 

44 app.logger.debug(f'sync seedbox ping: "{ping}"') 

45 

46 # Call ping.start() if enabled 

47 if ping: 

48 app.ping.start("sync_seedbox") 

49 

50 finished_path = app.seedboxsync_config.get("seedbox_finished_path", "") 

51 part_suffix = app.seedboxsync_config.get("seedbox_part_suffix") 

52 app.logger.debug(f'Scanning files in "{finished_path}"') 

53 

54 # Walk through all files on the seedbox 

55 try: 

56 try: 

57 app.sync.chdir(None) # type: ignore[arg-type] 

58 app.sync.chdir(finished_path) 

59 except FileNotFoundError as exc: 

60 app.logger.error(f"{exc!s}\nFailed to scan directory: {finished_path}") 

61 return 

62 

63 for root, _, filenames in app.sync.walk(""): # type: ignore[attr-defined] 

64 root = Path(root) 

65 

66 for filename in filenames: 

67 filepath = root / filename 

68 

69 if filepath.suffix == part_suffix: 

70 app.logger.debug(f'Skipping part file "{filename}"') 

71 elif Download.is_already_download(filepath): 

72 app.logger.debug(f'Skipping already downloaded file "{filename}"') 

73 elif __exclude_by_pattern(filepath): 

74 app.logger.debug(f'Skipping excluded file "{filename}"') 

75 else: 

76 if dry_run: 

77 app.logger.info(f'Dry-run: not downloading "{filepath}"') 

78 continue 

79 

80 __get_file(filepath, only_store) 

81 

82 except (OSError, FileNotFoundError) as exc: 

83 app.logger.error(f'SeedboxSyncError > "{exc}"') 

84 

85 # Call ping.success() if enabled 

86 if ping: 

87 app.ping.success("sync_seedbox") 

88 

89 

90def __exclude_by_pattern(filepath: str | PathLike[str]) -> bool: 

91 """ 

92 Determine if a file should be excluded from synchronization based on patterns. 

93 

94 Args: 

95 ctx (Context): The Click context object. 

96 filepath (str | PathLike[str]): Path of the file to check. 

97 

98 Returns: 

99 bool: True if the file matches the exclude pattern, False otherwise. 

100 

101 Raises: 

102 SeedboxSyncConfigurationError: If the exclude pattern is invalid. 

103 """ 

104 pattern = app.seedboxsync_config.get("seedbox_exclude_syncing", "") 

105 filepath = fspath(filepath) 

106 if not pattern: 

107 return False 

108 

109 try: 

110 return re.search(pattern, filepath) is not None 

111 except re.error as exc: 

112 raise SeedboxSyncConfigurationError("Invalid configuration for exclude_syncing! See https://docs.python.org/3/library/re.html") from exc 

113 

114 

115def __get_file(filepath: str | PathLike[str], only_store: bool) -> None: 

116 """ 

117 Download a single file from the seedbox. 

118 

119 Handles file path creation, partial download suffix, database persistence, 

120 size verification, and renaming after successful download. 

121 

122 Args: 

123 ctx (Context): The Click context object. 

124 filepath (str): Path of the file on the seedbox. 

125 only_store (bool): Whether to record the file without downloading it. 

126 """ 

127 filepath = fspath(filepath) 

128 local_filepath = Path(app.seedboxsync_config.get("local_download_path", "")).expanduser().resolve() / filepath 

129 part_suffix = app.seedboxsync_config.get("seedbox_part_suffix", "") 

130 local_filepath_part = local_filepath.with_suffix(part_suffix) 

131 local_path = local_filepath.expanduser().resolve().parent 

132 

133 # Create local directory tree if necessary 

134 if not only_store: 

135 utils.ensure_dir_exists(local_path) 

136 app.logger.debug(f'Download: "{filepath}" to "{local_path}"') 

137 

138 try: 

139 seedbox_size = app.sync.stat(filepath).st_size 

140 if seedbox_size == 0: 

141 app.logger.warning(f'Empty file: "{filepath}" ({seedbox_size!s})') 

142 

143 download = Download.create(path=filepath, seedbox_size=seedbox_size) 

144 download.save() 

145 

146 if not only_store: 

147 app.logger.info(f'Downloading "{filepath}"') 

148 progress_callback = DownloadProgress(download) 

149 app.sync.get(filepath, local_filepath_part, progress_callback=progress_callback) 

150 local_size = Path(local_filepath_part).stat().st_size 

151 

152 if local_size == 0 or local_size != seedbox_size: 

153 app.logger.error(f'Download failed: "{filepath}" ({local_size!s}/{seedbox_size!s})') 

154 

155 Path(local_filepath_part).rename(local_filepath) 

156 else: 

157 app.logger.info(f'Marking as downloaded "{filepath}"') 

158 local_size = seedbox_size 

159 

160 download.local_size = local_size 

161 download.finished = datetime.datetime.now() 

162 download.save() 

163 except SSHException as exc: 

164 app.logger.error(f"Download failed: {exc!s}")