Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 40 additions & 11 deletions .github/workflows/latest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.5.2
4.5.3-develop2
5 changes: 5 additions & 0 deletions config/config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ nohardlinks:
- BroadcasTheNet
# <OPTIONAL> ignore_root_dir var: Will ignore any hardlinks detected in the same root_dir (Default True).
ignore_root_dir: true
cross-seed:
# <OPTIONAL> ignore_root_dir var: Will ignore any hardlinks detected in the same root_dir (Default True).
ignore_root_dir: false
# <OPTIONAL> 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
Expand Down
1 change: 1 addition & 0 deletions docs/Config-Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | <center>❌</center> |
| `ignore_root_dir` | Ignore any hardlinks detected in the same [root_dir](#directory) | True | <center>❌</center> |
| `ignore_category_dir` | Ignore any hardlinks detected in the same [category_dir](#cat) | True | <center>❌</center> |

## **share_limits:**

Expand Down
7 changes: 6 additions & 1 deletion modules/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -480,13 +480,18 @@ 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"] = []
if not isinstance(self.nohardlinks[cat_str]["ignore_root_dir"], bool):
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"
Expand Down
8 changes: 7 additions & 1 deletion modules/core/tag_nohardlinks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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(
Expand Down
52 changes: 38 additions & 14 deletions modules/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions web-ui/js/config-schemas/nohardlinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down