diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml index 48be6f87..62595d3c 100644 --- a/.github/workflows/latest.yml +++ b/.github/workflows/latest.yml @@ -66,32 +66,61 @@ jobs: - name: Check Out Repo uses: actions/checkout@v4 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Update develop branch run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - # Checkout develop branch - git checkout develop + # Configure git with GitHub Actions bot credentials + git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + # Ensure we have the latest remote refs + git fetch origin + + # Check if develop branch exists locally, if not create it + if git show-ref --verify --quiet refs/heads/develop; then + echo "Local develop branch exists, checking it out" + git checkout develop + else + echo "Local develop branch doesn't exist, creating from remote" + git checkout -b develop origin/develop + fi # Reset develop to master + echo "Resetting develop branch to match master..." git reset --hard origin/master - # Read current version and bump minor patch + # Read current version and bump patch CURRENT_VERSION=$(cat VERSION) + echo "Current version: $CURRENT_VERSION" + IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" NEW_PATCH=$((PATCH + 1)) NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}-develop1" + echo "New version: $NEW_VERSION" + # Update VERSION file echo "$NEW_VERSION" > VERSION + # Check if there are changes to commit + if git diff --quiet; then + echo "No changes to commit" + exit 0 + fi + # Commit the change git add VERSION - git commit -m "Update VERSION to $NEW_VERSION" - - # Force push develop branch - git push --force origin develop + git commit -m "Update VERSION to $NEW_VERSION [skip ci]" + + # Push develop branch (force push since we reset to master) + # Note: This requires PAT_TOKEN secret with admin privileges to bypass branch protection + echo "Pushing changes to develop branch..." + if ! git push --force origin develop; then + echo "Failed to push to develop branch. This may be due to branch protection rules." + echo "Ensure PAT_TOKEN secret is set with admin privileges or disable branch protection for this workflow." + exit 1 + fi + + echo "Successfully updated develop branch to $NEW_VERSION" diff --git a/VERSION b/VERSION index 6cedcff6..71dd2442 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.5.2 +4.5.3-develop2 diff --git a/config/config.yml.sample b/config/config.yml.sample index 40e59691..79d57753 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -159,6 +159,11 @@ nohardlinks: - BroadcasTheNet # ignore_root_dir var: Will ignore any hardlinks detected in the same root_dir (Default True). ignore_root_dir: true + cross-seed: + # ignore_root_dir var: Will ignore any hardlinks detected in the same root_dir (Default True). + ignore_root_dir: false + # ignore_category_dir var: Will ignore any hardlinks detected in the same category save path (Default True). + ignore_category_dir: true share_limits: # Control how torrent share limits are set depending on the priority of your grouping diff --git a/docs/Config-Setup.md b/docs/Config-Setup.md index 0238a3bb..b46005f8 100644 --- a/docs/Config-Setup.md +++ b/docs/Config-Setup.md @@ -152,6 +152,7 @@ Beyond this you'll need to use one of the [categories](#cat) above as the key. | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------- | :----------------- | | `exclude_tags` | List of tags to exclude from the check. Torrents with any of these tags will not be processed. This is useful to exclude certain trackers from being scanned for hardlinking purposes | None |
| | `ignore_root_dir` | Ignore any hardlinks detected in the same [root_dir](#directory) | True |
| +| `ignore_category_dir` | Ignore any hardlinks detected in the same [category_dir](#cat) | True |
| ## **share_limits:** diff --git a/modules/config.py b/modules/config.py index cad4304b..fdc2b611 100755 --- a/modules/config.py +++ b/modules/config.py @@ -468,7 +468,7 @@ def process_config_nohardlinks(self): self.nohardlinks = {} for cat in self.data["nohardlinks"]: if isinstance(self.data["nohardlinks"], list) and isinstance(cat, str): - self.nohardlinks[cat] = {"exclude_tags": [], "ignore_root_dir": True} + self.nohardlinks[cat] = {"exclude_tags": [], "ignore_root_dir": True, "ignore_category_dir": True} continue if isinstance(cat, dict): cat_str = list(cat.keys())[0] @@ -480,6 +480,7 @@ def process_config_nohardlinks(self): self.nohardlinks[cat_str] = { "exclude_tags": cat[cat_str].get("exclude_tags", []), "ignore_root_dir": cat[cat_str].get("ignore_root_dir", True), + "ignore_category_dir": cat[cat_str].get("ignore_category_dir", True), } if self.nohardlinks[cat_str]["exclude_tags"] is None: self.nohardlinks[cat_str]["exclude_tags"] = [] @@ -487,6 +488,10 @@ def process_config_nohardlinks(self): err = f"Config Error: nohardlinks category {cat_str} attribute ignore_root_dir must be a boolean type" self.notify(err, "Config") raise Failed(err) + if not isinstance(self.nohardlinks[cat_str]["ignore_category_dir"], bool): + err = f"Config Error: nohardlinks category {cat_str} attribute ignore_category_dir must be a boolean type" + self.notify(err, "Config") + raise Failed(err) else: if self.commands["tag_nohardlinks"]: err = "Config Error: nohardlinks must be a list of categories" diff --git a/modules/core/tag_nohardlinks.py b/modules/core/tag_nohardlinks.py index 380f17e8..ca5a3933 100644 --- a/modules/core/tag_nohardlinks.py +++ b/modules/core/tag_nohardlinks.py @@ -88,13 +88,17 @@ def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, t self.torrents_updated_untagged.append(torrent.name) self.notify_attr_untagged.append(attr) - def _process_torrent_for_nohardlinks(self, torrent, check_hardlinks, ignore_root_dir, exclude_tags, category): + def _process_torrent_for_nohardlinks( + self, torrent, check_hardlinks, ignore_root_dir, exclude_tags, category, ignore_category_dir + ): """Helper method to process a single torrent for nohardlinks tagging.""" tracker = self.qbt.get_tags(self.qbt.get_tracker_urls(torrent.trackers)) has_nohardlinks = check_hardlinks.nohardlink( torrent["content_path"].replace(self.root_dir, self.remote_dir), self.config.notify, ignore_root_dir, + category, + ignore_category_dir, ) if any(util.is_tag_in_torrent(tag, torrent.tags) for tag in exclude_tags): # Skip to the next torrent if we find any torrents that are in the exclude tag @@ -128,6 +132,7 @@ def tag_nohardlinks(self): nohardlinks.get(torrent.category, {}).get("ignore_root_dir", True), nohardlinks.get(torrent.category, {}).get("exclude_tags", []), torrent.category, + nohardlinks.get(torrent.category, {}).get("ignore_category_dir", True), ) else: for category in nohardlinks: @@ -148,6 +153,7 @@ def tag_nohardlinks(self): nohardlinks.get(category, {}).get("ignore_root_dir", True), nohardlinks.get(category, {}).get("exclude_tags", []), category, + nohardlinks.get(category, {}).get("ignore_category_dir", True), ) if self.stats_tagged >= 1: logger.print_line( diff --git a/modules/util.py b/modules/util.py index 7de15642..cabfc4b4 100755 --- a/modules/util.py +++ b/modules/util.py @@ -738,17 +738,30 @@ def __init__(self, config): + get_root_files(self.orphaned_dir, "") + get_root_files(self.recycle_dir, "") ) - self.get_inode_count() + self.root_inode_count = self.get_inode_count() + categories = {name: path.replace(self.root_dir, self.remote_dir) for name, path in config.data["cat"].items()} + self.categories_inode_count = self.get_inode_count(categories) - def get_inode_count(self): - self.inode_count = {} + def get_inode_count(self, categories=None): + inode_count = {} for file in self.root_files: # Only check hardlinks for files that are symlinks if os.path.isfile(file) and os.path.islink(file): continue else: + mapped_path = file.replace(self.root_dir, self.remote_dir) + if not categories: + category = "root" + else: + category = None + for name, cat_path in categories.items(): + if mapped_path.startswith(cat_path): + category = name + break + if category is None: + continue # Not in any category, skip try: - inode_no = os.stat(file.replace(self.root_dir, self.remote_dir)).st_ino + inode_no = os.stat(mapped_path).st_ino except PermissionError as perm: logger.warning(f"{perm} : file {file} has permission issues. Skipping...") continue @@ -759,12 +772,15 @@ def get_inode_count(self): logger.stacktrace() logger.error(ex) continue - if inode_no in self.inode_count: - self.inode_count[inode_no] += 1 + if category not in inode_count: + inode_count[category] = {} + if inode_no in inode_count[category]: + inode_count[category][inode_no] += 1 else: - self.inode_count[inode_no] = 1 + inode_count[category][inode_no] = 1 + return inode_count["root"] if not categories else inode_count - def nohardlink(self, file, notify, ignore_root_dir): + def nohardlink(self, file, notify, ignore_root_dir, category, ignore_category_dir): """ Check if there are any hard links Will check if there are any hard links if it passes a file or folder @@ -773,19 +789,23 @@ def nohardlink(self, file, notify, ignore_root_dir): This fixes the bug in #192 """ - def has_hardlinks(self, file, ignore_root_dir): + def has_hardlinks(self, file, ignore_root_dir, category, ignore_category_dir): """ Check if a file has hard links. Args: file (str): The path to the file. ignore_root_dir (bool): Whether to ignore the root directory. + category (str): The category name. + ignore_category_dir (bool): Whether to ignore the torrent category directory. Returns: bool: True if the file has hard links, False otherwise. """ if ignore_root_dir: - return os.stat(file).st_nlink - self.inode_count.get(os.stat(file).st_ino, 1) > 0 + return os.stat(file).st_nlink - self.root_inode_count.get(os.stat(file).st_ino, 1) > 0 + elif ignore_category_dir: + return os.stat(file).st_nlink - self.categories_inode_count.get(category, {}).get(os.stat(file).st_ino, 1) > 0 else: return os.stat(file).st_nlink > 1 @@ -798,10 +818,11 @@ def has_hardlinks(self, file, ignore_root_dir): logger.trace(f"Checking file: {file}") logger.trace(f"Checking file inum: {os.stat(file).st_ino}") logger.trace(f"Checking no of hard links: {os.stat(file).st_nlink}") - logger.trace(f"Checking inode_count dict: {self.inode_count.get(os.stat(file).st_ino)}") + logger.trace(f"Checking root_inode_count dict: {self.root_inode_count.get(os.stat(file).st_ino)}") logger.trace(f"ignore_root_dir: {ignore_root_dir}") + logger.trace(f"ignore_category_dir: {ignore_category_dir} (category: {category})") # https://github.com/StuffAnThings/qbit_manage/issues/291 for more details - if has_hardlinks(self, file, ignore_root_dir): + if has_hardlinks(self, file, ignore_root_dir, category, ignore_category_dir): logger.trace(f"Hardlinks found in {file}.") check_for_hl = False else: @@ -830,9 +851,12 @@ def has_hardlinks(self, file, ignore_root_dir): logger.trace(f"Checking file inum: {os.stat(files).st_ino}") logger.trace(f"Checking file size: {file_size}") logger.trace(f"Checking no of hard links: {file_no_hardlinks}") - logger.trace(f"Checking inode_count dict: {self.inode_count.get(os.stat(files).st_ino)}") + logger.trace(f"Checking root_inode_count dict: {self.root_inode_count.get(os.stat(files).st_ino)}") logger.trace(f"ignore_root_dir: {ignore_root_dir}") - if has_hardlinks(self, files, ignore_root_dir) and file_size >= (largest_file_size * threshold): + logger.trace(f"ignore_category_dir: {ignore_category_dir} (category: {category})") + if has_hardlinks(self, files, ignore_root_dir, category, ignore_category_dir) and file_size >= ( + largest_file_size * threshold + ): logger.trace(f"Hardlinks found in {files}.") check_for_hl = False except PermissionError as perm: diff --git a/web-ui/js/config-schemas/nohardlinks.js b/web-ui/js/config-schemas/nohardlinks.js index be3d3f52..19fb1202 100644 --- a/web-ui/js/config-schemas/nohardlinks.js +++ b/web-ui/js/config-schemas/nohardlinks.js @@ -20,6 +20,12 @@ export const nohardlinksSchema = { label: 'Ignore Root Directory', description: 'If true, ignore hardlinks found within the same root directory.', default: true + }, + ignore_category_dir: { + type: 'boolean', + label: 'Ignore Category Directory', + description: 'If true, ignore hardlinks found within the same category directory.', + default: true } }, additionalProperties: false @@ -39,6 +45,12 @@ export const nohardlinksSchema = { label: 'Ignore Root Directory', description: 'If true, ignore hardlinks found within the same root directory.', default: true + }, + ignore_category_dir: { + type: 'boolean', + label: 'Ignore Category Directory', + description: 'If true, ignore hardlinks found within the same category directory.', + default: true } }, additionalProperties: false