Skip to content

Commit c680b03

Browse files
committed
feat: add repair databases
1 parent fc093f3 commit c680b03

10 files changed

Lines changed: 1311 additions & 7 deletions

File tree

TelegramDownloader/Data/FileService.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1942,6 +1942,28 @@ private async Task<List<ExpandoObject>> AddChildRecords(string id, string parent
19421942
var matchingFolder = Data.FirstOrDefault(x => x.FilePath + "/" == path)
19431943
?? Data.FirstOrDefault(x => x.FilePath == path.TrimEnd('/'));
19441944

1945+
// If not found in Data, try searching by name and parent FilterPath
1946+
// This handles cases where FilePath format doesn't match (e.g., first-level folders)
1947+
if (matchingFolder == null)
1948+
{
1949+
// Extract folder name and parent path from the navigation path
1950+
// e.g., "/FolderA/" -> name = "FolderA", parentPath = "/"
1951+
// e.g., "/Parent/Child/" -> name = "Child", parentPath = "/Parent/"
1952+
var pathParts = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
1953+
if (pathParts.Length > 0)
1954+
{
1955+
var folderName = pathParts[pathParts.Length - 1];
1956+
var parentPath = pathParts.Length == 1
1957+
? "/"
1958+
: "/" + string.Join("/", pathParts.Take(pathParts.Length - 1)) + "/";
1959+
1960+
// Search for folder by name and parent FilterPath
1961+
matchingFolder = collectionName == null
1962+
? await _db.getFolderByNameAndParentPath(dbName, folderName, parentPath)
1963+
: await _db.getFolderByNameAndParentPath(dbName, folderName, parentPath, collectionName);
1964+
}
1965+
}
1966+
19451967
if (matchingFolder != null)
19461968
{
19471969
childItem = matchingFolder.toFileManagerContent();

TelegramDownloader/Data/db/DbService.cs

Lines changed: 183 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,27 @@ public async Task<List<BsonFileManagerModel>> getFilesByParentId(string dbName,
467467
.ToListAsync();
468468
}
469469

470+
/// <summary>
471+
/// Find a folder by its name and parent FilterPath.
472+
/// This is useful for finding folders when FilePath format doesn't match the navigation path.
473+
/// </summary>
474+
public async Task<BsonFileManagerModel?> getFolderByNameAndParentPath(string dbName, string folderName, string parentFilterPath, string collectionName = "directory")
475+
{
476+
if (collectionName == null)
477+
collectionName = "directory";
478+
479+
// For first-level folders, FilterPath is "/"
480+
// For nested folders, FilterPath is the parent path (e.g., "/Parent/")
481+
var filter = Builders<BsonFileManagerModel>.Filter.Where(x =>
482+
!x.IsFile &&
483+
x.Name == folderName &&
484+
x.FilterPath == parentFilterPath);
485+
486+
return await (await getDatabase(dbName).GetCollection<BsonFileManagerModel>(collectionName)
487+
.FindAsync(filter))
488+
.FirstOrDefaultAsync();
489+
}
490+
470491
public async Task<List<int>> getAllIdsFromChannel(string dbName, string collectionName = "directory")
471492
{
472493
if (collectionName == null)
@@ -548,8 +569,9 @@ public async Task<BsonFileManagerModel> copyItem(string dbName, string sourceId,
548569
result.DateModified = DateTime.Now;
549570
result.FilterId = (target.FilterId ?? "") + target.Id + "/";
550571
result.ParentId = target.Id;
551-
// Both empty string and "/" indicate root folder
552-
var isRootTarget = string.IsNullOrEmpty(target.FilterPath) || target.FilterPath == "/";
572+
// Only empty string indicates root folder (the "Files" folder)
573+
// FilterPath "/" means a first-level folder, not the root itself
574+
var isRootTarget = string.IsNullOrEmpty(target.FilterPath);
553575
result.FilterPath = isRootTarget ? "/" : target.FilterPath + target.Name + "/";
554576
result.FilePath = isFile ? targetPath + result.Name : targetPath.TrimEnd('/');
555577
await getDatabase(dbName).GetCollection<BsonFileManagerModel>(collectionName).InsertOneAsync(result);
@@ -998,6 +1020,165 @@ public async Task<DatabaseStats> GetDatabaseStats(string dbName)
9981020
return stats;
9991021
}
10001022

1023+
/// <summary>
1024+
/// Analyze FilterPath issues without repairing them.
1025+
/// Returns detailed information about items that need repair.
1026+
/// </summary>
1027+
public async Task<FilterPathAnalysisResult> AnalyzeFilterPaths(string dbName, string collectionName = "directory")
1028+
{
1029+
if (collectionName == null)
1030+
collectionName = "directory";
1031+
1032+
var result = new FilterPathAnalysisResult { DatabaseName = dbName };
1033+
1034+
try
1035+
{
1036+
var collection = getDatabase(dbName).GetCollection<BsonFileManagerModel>(collectionName);
1037+
var allItems = await (await collection.FindAsync(Builders<BsonFileManagerModel>.Filter.Empty)).ToListAsync();
1038+
1039+
result.TotalItems = allItems.Count;
1040+
1041+
// Build a dictionary for quick lookup by Id
1042+
var itemsById = allItems.ToDictionary(x => x.Id, x => x);
1043+
1044+
foreach (var item in allItems)
1045+
{
1046+
// Skip the root folder (no ParentId)
1047+
if (string.IsNullOrEmpty(item.ParentId))
1048+
continue;
1049+
1050+
// Calculate the correct FilterPath and FilterId
1051+
var correctFilterPath = CalculateFilterPath(item.ParentId, itemsById);
1052+
var correctFilterId = CalculateFilterId(item.ParentId, itemsById);
1053+
var normalizedFilePath = item.FilePath?.Replace("\\", "/");
1054+
1055+
bool hasIssue = false;
1056+
1057+
if (item.FilterPath != correctFilterPath)
1058+
{
1059+
result.FilterPathIssues++;
1060+
hasIssue = true;
1061+
}
1062+
1063+
if (item.FilterId != correctFilterId)
1064+
{
1065+
result.FilterIdIssues++;
1066+
hasIssue = true;
1067+
}
1068+
1069+
if (normalizedFilePath != item.FilePath)
1070+
{
1071+
result.FilePathIssues++;
1072+
hasIssue = true;
1073+
}
1074+
1075+
if (hasIssue)
1076+
{
1077+
result.ItemsWithIssues++;
1078+
}
1079+
}
1080+
}
1081+
catch (Exception ex)
1082+
{
1083+
_logger.LogError(ex, "Error analyzing FilterPaths for database {DbName}", dbName);
1084+
result.Error = ex.Message;
1085+
}
1086+
1087+
return result;
1088+
}
1089+
1090+
/// <summary>
1091+
/// Repair FilterPath and FilterId for all items in a database.
1092+
/// This fixes data corruption caused by incorrect path calculations during move operations.
1093+
/// </summary>
1094+
public async Task<int> RepairFilterPaths(string dbName, string collectionName = "directory")
1095+
{
1096+
if (collectionName == null)
1097+
collectionName = "directory";
1098+
1099+
var collection = getDatabase(dbName).GetCollection<BsonFileManagerModel>(collectionName);
1100+
var allItems = await (await collection.FindAsync(Builders<BsonFileManagerModel>.Filter.Empty)).ToListAsync();
1101+
1102+
// Build a dictionary for quick lookup by Id
1103+
var itemsById = allItems.ToDictionary(x => x.Id, x => x);
1104+
1105+
int repairedCount = 0;
1106+
1107+
foreach (var item in allItems)
1108+
{
1109+
// Skip the root folder (no ParentId)
1110+
if (string.IsNullOrEmpty(item.ParentId))
1111+
continue;
1112+
1113+
// Calculate the correct FilterPath and FilterId by walking up the parent chain
1114+
var correctFilterPath = CalculateFilterPath(item.ParentId, itemsById);
1115+
var correctFilterId = CalculateFilterId(item.ParentId, itemsById);
1116+
1117+
// Also normalize FilePath (replace backslashes with forward slashes)
1118+
var normalizedFilePath = item.FilePath?.Replace("\\", "/");
1119+
1120+
bool needsUpdate = false;
1121+
var updateDef = Builders<BsonFileManagerModel>.Update.Combine();
1122+
1123+
if (item.FilterPath != correctFilterPath)
1124+
{
1125+
updateDef = updateDef.Set(x => x.FilterPath, correctFilterPath);
1126+
needsUpdate = true;
1127+
}
1128+
1129+
if (item.FilterId != correctFilterId)
1130+
{
1131+
updateDef = updateDef.Set(x => x.FilterId, correctFilterId);
1132+
needsUpdate = true;
1133+
}
1134+
1135+
if (normalizedFilePath != item.FilePath)
1136+
{
1137+
updateDef = updateDef.Set(x => x.FilePath, normalizedFilePath);
1138+
needsUpdate = true;
1139+
}
1140+
1141+
if (needsUpdate)
1142+
{
1143+
await collection.UpdateOneAsync(
1144+
Builders<BsonFileManagerModel>.Filter.Eq(x => x.Id, item.Id),
1145+
updateDef);
1146+
repairedCount++;
1147+
}
1148+
}
1149+
1150+
_logger.LogInformation("Repaired {Count} items in database {DbName}", repairedCount, dbName);
1151+
return repairedCount;
1152+
}
1153+
1154+
private string CalculateFilterPath(string parentId, Dictionary<string, BsonFileManagerModel> itemsById)
1155+
{
1156+
if (string.IsNullOrEmpty(parentId) || !itemsById.TryGetValue(parentId, out var parent))
1157+
return "/";
1158+
1159+
// If parent is root (no ParentId), return "/"
1160+
if (string.IsNullOrEmpty(parent.ParentId))
1161+
return "/";
1162+
1163+
// Otherwise, build the path recursively
1164+
var parentPath = CalculateFilterPath(parent.ParentId, itemsById);
1165+
return parentPath + parent.Name + "/";
1166+
}
1167+
1168+
private string CalculateFilterId(string parentId, Dictionary<string, BsonFileManagerModel> itemsById)
1169+
{
1170+
if (string.IsNullOrEmpty(parentId) || !itemsById.TryGetValue(parentId, out var parent))
1171+
return "";
1172+
1173+
// If parent is root (no ParentId), return just the parent's Id
1174+
if (string.IsNullOrEmpty(parent.ParentId))
1175+
return parent.Id + "/";
1176+
1177+
// Otherwise, build the FilterId recursively
1178+
var parentFilterId = CalculateFilterId(parent.ParentId, itemsById);
1179+
return parentFilterId + parent.Id + "/";
1180+
}
1181+
10011182
#endregion
10021183

10031184
}

TelegramDownloader/Data/db/IDbService.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public interface IDbService
3636
Task<List<BsonFileManagerModel>> getAllFolders(string dbName, string? parentId = null, string collectionName = "directory");
3737
Task<List<BsonFileManagerModel>> getFoldersByParentId(string dbName, string? parentId, string collectionName = "directory");
3838
Task<List<BsonFileManagerModel>> getFilesByParentId(string dbName, string parentId, string collectionName = "directory");
39+
Task<BsonFileManagerModel?> getFolderByNameAndParentPath(string dbName, string folderName, string parentFilterPath, string collectionName = "directory");
3940
Task<List<int>> getAllIdsFromChannel(string dbName, string collectionName = "directory");
4041
IMongoDatabase getDatabase(string dbName);
4142
Task<BsonFileManagerModel> getEntry(string dbName, string filterId, string name, string collectionName = "directory");
@@ -71,6 +72,8 @@ public interface IDbService
7172
// Maintenance operations
7273
Task<List<string>> GetAllChannelDatabaseNames();
7374
Task<DatabaseStats> GetDatabaseStats(string dbName);
75+
Task<FilterPathAnalysisResult> AnalyzeFilterPaths(string dbName, string collectionName = "directory");
76+
Task<int> RepairFilterPaths(string dbName, string collectionName = "directory");
7477
}
7578

7679
public class DatabaseStats
@@ -80,4 +83,21 @@ public class DatabaseStats
8083
public DateTime? CreatedAt { get; set; }
8184
public DateTime? LastModified { get; set; }
8285
}
86+
87+
public class FilterPathAnalysisResult
88+
{
89+
public string DatabaseName { get; set; } = "";
90+
public int TotalItems { get; set; }
91+
public int ItemsWithIssues { get; set; }
92+
public int FilterPathIssues { get; set; }
93+
public int FilterIdIssues { get; set; }
94+
public int FilePathIssues { get; set; }
95+
public string? Error { get; set; }
96+
public bool HasIssues => ItemsWithIssues > 0;
97+
public bool IsSelected { get; set; }
98+
public bool IsAnalyzed { get; set; }
99+
public bool IsRepairing { get; set; }
100+
public bool IsRepaired { get; set; }
101+
public int RepairedCount { get; set; }
102+
}
83103
}

TelegramDownloader/Models/FileModel.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,16 @@ public BsonFileManagerModel(string Path,string FolderName, FileManagerDirectoryC
6262
this.ParentId = parent.Id;
6363
this.CaseSensitive = false;
6464
this.Name = FolderName;
65-
this.FilePath = System.IO.Path.Combine(Path, FolderName);
66-
this.FilterId = (string.IsNullOrEmpty(parent.FilterId) ? String.Concat(parent.Id, "/") : String.Concat(System.IO.Path.Combine(parent.FilterId, parent.Id), "/"));
65+
// Use forward slashes consistently (not System.IO.Path.Combine which uses backslashes on Windows)
66+
var normalizedPath = Path?.Replace("\\", "/") ?? "";
67+
if (!string.IsNullOrEmpty(normalizedPath) && !normalizedPath.EndsWith("/"))
68+
normalizedPath += "/";
69+
this.FilePath = normalizedPath + FolderName;
70+
// Use forward slashes for FilterId as well
71+
var parentFilterId = parent.FilterId?.Replace("\\", "/") ?? "";
72+
this.FilterId = string.IsNullOrEmpty(parentFilterId)
73+
? parent.Id + "/"
74+
: parentFilterId + parent.Id + "/";
6775
this.FilterPath = (string.IsNullOrEmpty(parent.FilterId) ? "/" : String.Concat(parent.FilterPath, parent.Name, "/"));
6876
this.ShowHiddenItems = false;
6977
this.IsFile = false;

TelegramDownloader/Models/GeneralConfig.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,13 @@ public class GeneralConfig
105105
/// </summary>
106106
public bool EnableVideoTranscoding { get; set; } = false;
107107

108+
// Refresh Data Settings
109+
/// <summary>
110+
/// Enable refresh data option for channels where you are admin/owner.
111+
/// By default, refresh is only available for channels you don't own.
112+
/// </summary>
113+
public bool EnableRefreshOwnChannels { get; set; } = false;
114+
108115
}
109116

110117
public class TLConfig

TelegramDownloader/Pages/Config.razor

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,36 @@
488488
</div>
489489
</div>
490490

491+
<!-- Data Refresh Section -->
492+
<div class="config-section">
493+
<div class="config-section-header">
494+
<i class="bi bi-arrow-clockwise"></i>
495+
<span>Data Refresh</span>
496+
</div>
497+
<div class="config-section-body">
498+
<div class="config-item">
499+
<div class="config-item-info">
500+
<div class="config-item-label">
501+
<i class="bi bi-person-badge"></i>
502+
Enable Refresh for Own Channels
503+
</div>
504+
<div class="config-item-description">
505+
Allow refreshing data from Telegram for channels where you are admin or owner.
506+
By default, refresh is only available for channels you don't own (to sync changes made by others).
507+
<div class="alert alert-warning py-2 px-3 mb-0 mt-2" style="font-size: 0.85rem;">
508+
<i class="bi bi-exclamation-triangle me-2"></i>
509+
<strong>Warning:</strong> Refreshing a channel re-downloads all file metadata from Telegram.
510+
This can take a long time for large channels.
511+
</div>
512+
</div>
513+
</div>
514+
<div class="config-item-control">
515+
<InputCheckbox @bind-Value="Model!.EnableRefreshOwnChannels" class="form-check-input config-switch" />
516+
</div>
517+
</div>
518+
</div>
519+
</div>
520+
491521
<!-- Interface Section -->
492522
<div class="config-section">
493523
<div class="config-section-header">
@@ -764,6 +794,24 @@
764794
</button>
765795
</div>
766796
</div>
797+
798+
<div class="config-item">
799+
<div class="config-item-info">
800+
<div class="config-item-label">
801+
<i class="bi bi-wrench-adjustable"></i>
802+
Path Repair Tool
803+
</div>
804+
<div class="config-item-description">
805+
Analyze and repair file path issues in your databases. Use this if files disappear after moving folders or if STRM exports are missing files.
806+
</div>
807+
</div>
808+
<div class="config-item-control">
809+
<button type="button" class="btn btn-outline-warning btn-sm d-flex align-items-center gap-2" @onclick="OpenPathRepairModal">
810+
<i class="bi bi-wrench"></i>
811+
<span>Repair</span>
812+
</button>
813+
</div>
814+
</div>
767815
</div>
768816
</div>
769817

@@ -940,6 +988,11 @@
940988
MainLayout.OpenDatabaseMaintenanceModal();
941989
}
942990

991+
private void OpenPathRepairModal()
992+
{
993+
MainLayout.OpenPathRepairModal();
994+
}
995+
943996
private void RefreshOrphanedFavoritesCount()
944997
{
945998
try

0 commit comments

Comments
 (0)