mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 02:48:38 -04:00
Handling tags directory cleanup when the original files are changed.
This commit is contained in:
parent
1deaeb3144
commit
0e9a0a3f79
3 changed files with 143 additions and 2 deletions
|
|
@ -559,9 +559,43 @@ def _sync_tags_directory_tag_files(tags_directory: pathlib.Path, session: Sessio
|
|||
.outerjoin(Tag, FileGroup).all()
|
||||
links = list()
|
||||
for tag, tag_file, file_group in tag_files:
|
||||
for file, link in file_group.get_tag_directory_paths_map().items():
|
||||
paths_map = file_group.get_tag_directory_paths_map()
|
||||
|
||||
# Check if ANY source file still exists in this FileGroup.
|
||||
any_source_exists = any(file.is_file() for file in paths_map.keys())
|
||||
|
||||
for file, link in paths_map.items():
|
||||
link = tags_directory / link
|
||||
|
||||
if not file.is_file():
|
||||
if any_source_exists:
|
||||
# Partial deletion - remove stale hardlink for this file.
|
||||
if link.is_file():
|
||||
logger.debug(f'Removing stale hardlink (source deleted): {link}')
|
||||
link.unlink()
|
||||
# Don't add to links - nothing should exist here.
|
||||
continue
|
||||
else:
|
||||
# ALL files deleted - preserve hardlinks as safety measure.
|
||||
if link.is_file():
|
||||
links.append(link)
|
||||
continue
|
||||
|
||||
need_create = False
|
||||
if not link.is_file():
|
||||
need_create = True
|
||||
else:
|
||||
# Check if existing link points to the correct file (same inode).
|
||||
# When a source file is replaced, the hardlink becomes stale.
|
||||
try:
|
||||
if link.stat().st_ino != file.stat().st_ino:
|
||||
# Hardlink is stale - points to old version of file.
|
||||
link.unlink()
|
||||
need_create = True
|
||||
except FileNotFoundError:
|
||||
need_create = True
|
||||
|
||||
if need_create:
|
||||
try:
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
link.hardlink_to(file)
|
||||
|
|
|
|||
|
|
@ -512,6 +512,113 @@ async def test_tag_rename_with_channel(test_session, test_directory, video_facto
|
|||
tag.delete()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tags_directory_file_replaced(test_session, test_directory, tag_factory, video_bytes, await_switches):
|
||||
"""Tags directory hardlinks should update when source file is replaced."""
|
||||
from wrolpi import tags as tags_module
|
||||
|
||||
# Create a video file and its FileGroup.
|
||||
video_file = test_directory / 'video.mp4'
|
||||
video_file.write_bytes(video_bytes)
|
||||
fg = FileGroup.from_paths(test_session, video_file)
|
||||
test_session.add(fg)
|
||||
test_session.commit()
|
||||
original_inode = video_file.stat().st_ino
|
||||
|
||||
# Tag the FileGroup.
|
||||
tag = await tag_factory('favorite')
|
||||
fg.add_tag(test_session, tag.id)
|
||||
await await_switches()
|
||||
|
||||
# Verify hardlink points to original inode.
|
||||
tag_link = test_directory / 'tags' / tag.name / video_file.name
|
||||
assert tag_link.is_file(), 'Tag link should exist'
|
||||
assert tag_link.stat().st_ino == original_inode, 'Hardlink should point to original file'
|
||||
|
||||
# Replace the source file (same name, different inode).
|
||||
video_file.unlink()
|
||||
video_file.write_bytes(b'new content')
|
||||
new_inode = video_file.stat().st_ino
|
||||
assert new_inode != original_inode, 'New file should have different inode'
|
||||
|
||||
# Sync and verify hardlink points to new inode.
|
||||
tags_module.sync_tags_directory()
|
||||
assert tag_link.stat().st_ino == new_inode, 'Hardlink should point to new file after sync'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tags_directory_partial_file_deleted(test_session, test_directory, tag_factory, video_bytes,
|
||||
image_bytes_factory, await_switches):
|
||||
"""When one file in a FileGroup is deleted, its hardlink should be removed."""
|
||||
from wrolpi import tags as tags_module
|
||||
|
||||
# Create a video with poster.
|
||||
video_file = test_directory / 'video.mp4'
|
||||
poster_file = test_directory / 'video.jpg'
|
||||
video_file.write_bytes(video_bytes)
|
||||
poster_file.write_bytes(image_bytes_factory())
|
||||
|
||||
fg = FileGroup.from_paths(test_session, video_file, poster_file)
|
||||
test_session.add(fg)
|
||||
test_session.commit()
|
||||
|
||||
# Tag the FileGroup.
|
||||
tag = await tag_factory('favorite')
|
||||
fg.add_tag(test_session, tag.id)
|
||||
await await_switches()
|
||||
|
||||
# Verify both hardlinks exist.
|
||||
video_link = test_directory / 'tags' / tag.name / 'video.mp4'
|
||||
poster_link = test_directory / 'tags' / tag.name / 'video.jpg'
|
||||
assert video_link.is_file()
|
||||
assert poster_link.is_file()
|
||||
|
||||
# Delete the poster file from source (partial deletion).
|
||||
poster_file.unlink()
|
||||
|
||||
# Sync and verify poster hardlink is removed, video link preserved.
|
||||
tags_module.sync_tags_directory()
|
||||
assert video_link.is_file(), 'Video link should still exist'
|
||||
assert not poster_link.exists(), 'Poster link should be removed after source deleted'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tags_directory_all_files_deleted(test_session, test_directory, tag_factory, video_bytes,
|
||||
image_bytes_factory, await_switches):
|
||||
"""When ALL files in a FileGroup are deleted, hardlinks should be preserved."""
|
||||
from wrolpi import tags as tags_module
|
||||
|
||||
# Create a video with poster.
|
||||
video_file = test_directory / 'video.mp4'
|
||||
poster_file = test_directory / 'video.jpg'
|
||||
video_file.write_bytes(video_bytes)
|
||||
poster_file.write_bytes(image_bytes_factory())
|
||||
|
||||
fg = FileGroup.from_paths(test_session, video_file, poster_file)
|
||||
test_session.add(fg)
|
||||
test_session.commit()
|
||||
|
||||
# Tag the FileGroup.
|
||||
tag = await tag_factory('favorite')
|
||||
fg.add_tag(test_session, tag.id)
|
||||
await await_switches()
|
||||
|
||||
# Verify both hardlinks exist.
|
||||
video_link = test_directory / 'tags' / tag.name / 'video.mp4'
|
||||
poster_link = test_directory / 'tags' / tag.name / 'video.jpg'
|
||||
assert video_link.is_file()
|
||||
assert poster_link.is_file()
|
||||
|
||||
# Delete ALL source files.
|
||||
video_file.unlink()
|
||||
poster_file.unlink()
|
||||
|
||||
# Sync and verify ALL hardlinks are preserved (safety measure).
|
||||
tags_module.sync_tags_directory()
|
||||
assert video_link.is_file(), 'Video link should be preserved when all sources deleted'
|
||||
assert poster_link.is_file(), 'Poster link should be preserved when all sources deleted'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tags_directory_disabled(test_session, test_directory, tag_factory, video_factory, await_switches):
|
||||
"""Test that Tag Directory is NOT synchronized when tags_directory setting is disabled."""
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.17.5-beta
|
||||
0.17.6-beta
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue