fix orphaned file discovery

This commit is contained in:
Stefan Hardegger
2025-09-27 08:46:17 +02:00
parent c2e5445196
commit 2b4cb1456f

View File

@@ -462,6 +462,7 @@ public class ImageService {
Files.walk(contentImagesDir, 2) Files.walk(contentImagesDir, 2)
.filter(Files::isDirectory) .filter(Files::isDirectory)
.filter(path -> !path.equals(contentImagesDir)) // Skip the root content directory .filter(path -> !path.equals(contentImagesDir)) // Skip the root content directory
.filter(path -> !isSynologySystemPath(path)) // Skip Synology system directories
.forEach(storyDir -> { .forEach(storyDir -> {
try { try {
String storyId = storyDir.getFileName().toString(); String storyId = storyDir.getFileName().toString();
@@ -476,6 +477,8 @@ public class ImageService {
try { try {
Files.walk(storyDir) Files.walk(storyDir)
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
.filter(path -> !isSynologySystemPath(path)) // Skip Synology system files
.filter(path -> isValidImageFile(path)) // Only process actual image files
.forEach(file -> { .forEach(file -> {
try { try {
long size = Files.size(file); long size = Files.size(file);
@@ -495,13 +498,18 @@ public class ImageService {
try { try {
Files.walk(storyDir) Files.walk(storyDir)
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
.filter(path -> !isSynologySystemPath(path)) // Skip Synology system files
.filter(path -> isValidImageFile(path)) // Only process actual image files
.forEach(imageFile -> { .forEach(imageFile -> {
try { try {
String imagePath = getRelativeImagePath(imageFile); String filename = imageFile.getFileName().toString();
if (!referencedImages.contains(imagePath)) { // Only consider it orphaned if it's not in our referenced filenames
logger.debug("Found orphaned image: {}", imagePath); if (!referencedImages.contains(filename)) {
logger.debug("Found orphaned image: {}", filename);
orphanedImages.add(imageFile.toString()); orphanedImages.add(imageFile.toString());
} else {
logger.debug("Image file is referenced, keeping: {}", filename);
} }
} catch (Exception e) { } catch (Exception e) {
errors.add("Error checking image file " + imageFile + ": " + e.getMessage()); errors.add("Error checking image file " + imageFile + ": " + e.getMessage());
@@ -583,10 +591,10 @@ public class ImageService {
} }
/** /**
* Collect all image references from all story content * Collect all image filenames referenced in content (UUID-based filenames only)
*/ */
private Set<String> collectAllImageReferences() { private Set<String> collectAllImageReferences() {
Set<String> referencedImages = new HashSet<>(); Set<String> referencedFilenames = new HashSet<>();
try { try {
// Get all stories // Get all stories
@@ -596,21 +604,21 @@ public class ImageService {
Pattern imagePattern = Pattern.compile("src=[\"']([^\"']*(?:content/[^\"']*\\.(jpg|jpeg|png)))[\"']", Pattern.CASE_INSENSITIVE); Pattern imagePattern = Pattern.compile("src=[\"']([^\"']*(?:content/[^\"']*\\.(jpg|jpeg|png)))[\"']", Pattern.CASE_INSENSITIVE);
for (com.storycove.entity.Story story : allStories) { for (com.storycove.entity.Story story : allStories) {
// Add story cover image if present // Add story cover image filename if present
if (story.getCoverPath() != null && !story.getCoverPath().trim().isEmpty()) { if (story.getCoverPath() != null && !story.getCoverPath().trim().isEmpty()) {
String relativePath = convertAbsolutePathToRelative(story.getCoverPath()); String filename = extractFilename(story.getCoverPath());
if (relativePath != null) { if (filename != null) {
referencedImages.add(relativePath); referencedFilenames.add(filename);
logger.debug("Found cover image reference in story {}: {}", story.getId(), relativePath); logger.debug("Found cover image filename in story {}: {}", story.getId(), filename);
} }
} }
// Add author avatar image if present // Add author avatar image filename if present
if (story.getAuthor() != null && story.getAuthor().getAvatarImagePath() != null && !story.getAuthor().getAvatarImagePath().trim().isEmpty()) { if (story.getAuthor() != null && story.getAuthor().getAvatarImagePath() != null && !story.getAuthor().getAvatarImagePath().trim().isEmpty()) {
String relativePath = convertAbsolutePathToRelative(story.getAuthor().getAvatarImagePath()); String filename = extractFilename(story.getAuthor().getAvatarImagePath());
if (relativePath != null) { if (filename != null) {
referencedImages.add(relativePath); referencedFilenames.add(filename);
logger.debug("Found avatar image reference for author {}: {}", story.getAuthor().getId(), relativePath); logger.debug("Found avatar image filename for author {}: {}", story.getAuthor().getId(), filename);
} }
} }
@@ -621,11 +629,11 @@ public class ImageService {
while (matcher.find()) { while (matcher.find()) {
String imageSrc = matcher.group(1); String imageSrc = matcher.group(1);
// Convert to relative path format that matches our file system // Extract just the filename from the URL
String relativePath = convertSrcToRelativePath(imageSrc); String filename = extractFilename(imageSrc);
if (relativePath != null) { if (filename != null && isUuidBasedFilename(filename)) {
referencedImages.add(relativePath); referencedFilenames.add(filename);
logger.debug("Found content image reference in story {}: {}", story.getId(), relativePath); logger.debug("Found content image filename in story {}: {}", story.getId(), filename);
} }
} }
} }
@@ -635,10 +643,10 @@ public class ImageService {
List<com.storycove.entity.Author> allAuthors = authorService.findAll(); List<com.storycove.entity.Author> allAuthors = authorService.findAll();
for (com.storycove.entity.Author author : allAuthors) { for (com.storycove.entity.Author author : allAuthors) {
if (author.getAvatarImagePath() != null && !author.getAvatarImagePath().trim().isEmpty()) { if (author.getAvatarImagePath() != null && !author.getAvatarImagePath().trim().isEmpty()) {
String relativePath = convertAbsolutePathToRelative(author.getAvatarImagePath()); String filename = extractFilename(author.getAvatarImagePath());
if (relativePath != null) { if (filename != null) {
referencedImages.add(relativePath); referencedFilenames.add(filename);
logger.debug("Found standalone avatar image reference for author {}: {}", author.getId(), relativePath); logger.debug("Found standalone avatar image filename for author {}: {}", author.getId(), filename);
} }
} }
} }
@@ -647,10 +655,10 @@ public class ImageService {
List<com.storycove.entity.Collection> allCollections = collectionService.findAllWithTags(); List<com.storycove.entity.Collection> allCollections = collectionService.findAllWithTags();
for (com.storycove.entity.Collection collection : allCollections) { for (com.storycove.entity.Collection collection : allCollections) {
if (collection.getCoverImagePath() != null && !collection.getCoverImagePath().trim().isEmpty()) { if (collection.getCoverImagePath() != null && !collection.getCoverImagePath().trim().isEmpty()) {
String relativePath = convertAbsolutePathToRelative(collection.getCoverImagePath()); String filename = extractFilename(collection.getCoverImagePath());
if (relativePath != null) { if (filename != null) {
referencedImages.add(relativePath); referencedFilenames.add(filename);
logger.debug("Found collection cover image reference for collection {}: {}", collection.getId(), relativePath); logger.debug("Found collection cover image filename for collection {}: {}", collection.getId(), filename);
} }
} }
} }
@@ -659,7 +667,7 @@ public class ImageService {
logger.error("Error collecting image references from stories", e); logger.error("Error collecting image references from stories", e);
} }
return referencedImages; return referencedFilenames;
} }
/** /**
@@ -848,4 +856,82 @@ public class ImageService {
return String.format("%.1f GB", totalSizeBytes / (1024.0 * 1024.0 * 1024.0)); return String.format("%.1f GB", totalSizeBytes / (1024.0 * 1024.0 * 1024.0));
} }
} }
/**
* Check if a path is a Synology system path that should be ignored
*/
private boolean isSynologySystemPath(Path path) {
String pathStr = path.toString();
String fileName = path.getFileName().toString();
// Skip Synology metadata directories and files
return pathStr.contains("@eaDir") ||
fileName.startsWith("@") ||
fileName.contains("@SynoEAStream") ||
fileName.startsWith(".") ||
fileName.equals("Thumbs.db") ||
fileName.equals(".DS_Store");
}
/**
* Check if a file is a valid image file (not a system/metadata file)
*/
private boolean isValidImageFile(Path path) {
if (isSynologySystemPath(path)) {
return false;
}
String fileName = path.getFileName().toString().toLowerCase();
return fileName.endsWith(".jpg") ||
fileName.endsWith(".jpeg") ||
fileName.endsWith(".png") ||
fileName.endsWith(".gif") ||
fileName.endsWith(".webp");
}
/**
* Extract filename from a path or URL
*/
private String extractFilename(String pathOrUrl) {
if (pathOrUrl == null || pathOrUrl.trim().isEmpty()) {
return null;
}
try {
// Remove query parameters if present
if (pathOrUrl.contains("?")) {
pathOrUrl = pathOrUrl.substring(0, pathOrUrl.indexOf("?"));
}
// Get the last part after slash
String filename = pathOrUrl.substring(pathOrUrl.lastIndexOf("/") + 1);
// Remove any special Synology suffixes
filename = filename.replace("@SynoEAStream", "");
return filename.trim().isEmpty() ? null : filename;
} catch (Exception e) {
logger.debug("Failed to extract filename from: {}", pathOrUrl);
return null;
}
}
/**
* Check if a filename follows UUID pattern (indicates it's our generated file)
*/
private boolean isUuidBasedFilename(String filename) {
if (filename == null || filename.trim().isEmpty()) {
return false;
}
// Remove extension
String nameWithoutExt = filename;
int lastDot = filename.lastIndexOf(".");
if (lastDot > 0) {
nameWithoutExt = filename.substring(0, lastDot);
}
// Check if it matches UUID pattern (8-4-4-4-12 hex characters)
return nameWithoutExt.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
}
} }