Skip to content

Commit 13ac5c7

Browse files
committed
feat: maintenance: clean orphaned databases
1 parent 7aca7aa commit 13ac5c7

9 files changed

Lines changed: 1429 additions & 0 deletions

File tree

TelegramDownloader/Data/ITelegramService.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ public interface ITelegramService
4343
Task<Message> uploadFile(string chatId, Stream file, string fileName, string mimeType = null, UploadModel um = null, string caption = null);
4444
Task<List<TelegramChatDocuments>> searchAllChannelFiles(long id, int lastId);
4545
bool isMyChat(long id);
46+
bool isChannelOwner(long id);
4647
Task<TL.Channel?> CreateChannel(string title, string about);
48+
Task LeaveChannel(long id);
49+
Task DeleteChannel(long id);
50+
(string? name, bool exists) GetChannelInfo(long id);
4751
}
4852
}

TelegramDownloader/Data/TelegramService.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,97 @@ public bool isMyChat(long id)
359359
return true;
360360
}
361361

362+
public bool isChannelOwner(long id)
363+
{
364+
if (chats == null)
365+
return false;
366+
var peer = chats.chats[id];
367+
if (peer is TL.Channel channel)
368+
{
369+
return channel.IsActive && channel.flags.HasFlag(TL.Channel.Flags.creator);
370+
}
371+
return false;
372+
}
373+
374+
public async Task LeaveChannel(long id)
375+
{
376+
try
377+
{
378+
_logger.LogInformation("Leaving channel with ID: {Id}", id);
379+
var peer = chats.chats[id];
380+
if (peer is TL.Channel channel)
381+
{
382+
var inputChannel = new InputChannel(channel.id, channel.access_hash);
383+
await client.Channels_LeaveChannel(inputChannel);
384+
_logger.LogInformation("Successfully left channel: {Id}", id);
385+
}
386+
else
387+
{
388+
throw new Exception("The specified chat is not a channel");
389+
}
390+
}
391+
catch (Exception ex)
392+
{
393+
_logger.LogError(ex, "Error leaving channel: {Id}", id);
394+
throw;
395+
}
396+
}
397+
398+
public async Task DeleteChannel(long id)
399+
{
400+
try
401+
{
402+
_logger.LogInformation("Deleting channel with ID: {Id}", id);
403+
var peer = chats.chats[id];
404+
if (peer is TL.Channel channel)
405+
{
406+
if (!channel.flags.HasFlag(TL.Channel.Flags.creator))
407+
{
408+
throw new Exception("You are not the owner of this channel");
409+
}
410+
var inputChannel = new InputChannel(channel.id, channel.access_hash);
411+
await client.Channels_DeleteChannel(inputChannel);
412+
_logger.LogInformation("Successfully deleted channel: {Id}", id);
413+
}
414+
else
415+
{
416+
throw new Exception("The specified chat is not a channel");
417+
}
418+
}
419+
catch (Exception ex)
420+
{
421+
_logger.LogError(ex, "Error deleting channel: {Id}", id);
422+
throw;
423+
}
424+
}
425+
426+
public (string? name, bool exists) GetChannelInfo(long id)
427+
{
428+
try
429+
{
430+
if (chats == null || !chats.chats.ContainsKey(id))
431+
{
432+
return (null, false);
433+
}
434+
435+
var peer = chats.chats[id];
436+
if (peer is TL.Channel channel)
437+
{
438+
return (channel.title, true);
439+
}
440+
else if (peer is TL.Chat chat)
441+
{
442+
return (chat.title, true);
443+
}
444+
445+
return (peer.ToString(), true);
446+
}
447+
catch
448+
{
449+
return (null, false);
450+
}
451+
}
452+
362453
public async Task<List<ChatViewBase>> GetFouriteChannels(bool mustRefresh = true)
363454
{
364455

TelegramDownloader/Data/db/DbService.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,5 +903,102 @@ public async Task ClearAllTasks()
903903

904904
#endregion
905905

906+
#region Maintenance Operations
907+
908+
/// <summary>
909+
/// Get all database names that represent Telegram channels (numeric IDs)
910+
/// Excludes system databases like TCCONFIG, TFM-SHARED, admin, local, config
911+
/// </summary>
912+
public async Task<List<string>> GetAllChannelDatabaseNames()
913+
{
914+
var excludedDatabases = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
915+
{
916+
CONFIG_DB_NAME,
917+
SHARED_DB_NAME,
918+
"admin",
919+
"local",
920+
"config",
921+
"default"
922+
};
923+
924+
var databaseNames = new List<string>();
925+
926+
using (var cursor = await client.ListDatabaseNamesAsync())
927+
{
928+
while (await cursor.MoveNextAsync())
929+
{
930+
foreach (var dbName in cursor.Current)
931+
{
932+
// Only include databases that are numeric (channel IDs) or start with "-" (group IDs)
933+
if (!excludedDatabases.Contains(dbName) &&
934+
(long.TryParse(dbName, out _) || (dbName.StartsWith("-") && long.TryParse(dbName, out _))))
935+
{
936+
databaseNames.Add(dbName);
937+
}
938+
}
939+
}
940+
}
941+
942+
_logger.LogInformation("Found {Count} channel databases", databaseNames.Count);
943+
return databaseNames;
944+
}
945+
946+
/// <summary>
947+
/// Get statistics for a specific database including size, document count, and dates
948+
/// </summary>
949+
public async Task<DatabaseStats> GetDatabaseStats(string dbName)
950+
{
951+
var stats = new DatabaseStats();
952+
953+
try
954+
{
955+
var database = getDatabase(dbName);
956+
957+
// Get database stats using command
958+
var command = new BsonDocument { { "dbStats", 1 } };
959+
var result = await database.RunCommandAsync<BsonDocument>(command);
960+
961+
if (result.Contains("dataSize"))
962+
{
963+
stats.SizeInBytes = result["dataSize"].ToInt64();
964+
}
965+
966+
// Get document count from the directory collection
967+
var collection = database.GetCollection<BsonFileManagerModel>("directory");
968+
stats.DocumentCount = await collection.CountDocumentsAsync(Builders<BsonFileManagerModel>.Filter.Empty);
969+
970+
// Get creation date (oldest document) and last modified (newest document)
971+
var oldestDoc = await collection
972+
.Find(Builders<BsonFileManagerModel>.Filter.Empty)
973+
.Sort(Builders<BsonFileManagerModel>.Sort.Ascending(x => x.DateCreated))
974+
.Limit(1)
975+
.FirstOrDefaultAsync();
976+
977+
var newestDoc = await collection
978+
.Find(Builders<BsonFileManagerModel>.Filter.Empty)
979+
.Sort(Builders<BsonFileManagerModel>.Sort.Descending(x => x.DateModified))
980+
.Limit(1)
981+
.FirstOrDefaultAsync();
982+
983+
if (oldestDoc != null)
984+
{
985+
stats.CreatedAt = oldestDoc.DateCreated;
986+
}
987+
988+
if (newestDoc != null)
989+
{
990+
stats.LastModified = newestDoc.DateModified;
991+
}
992+
}
993+
catch (Exception ex)
994+
{
995+
_logger.LogWarning(ex, "Error getting stats for database {DbName}", dbName);
996+
}
997+
998+
return stats;
999+
}
1000+
1001+
#endregion
1002+
9061003
}
9071004
}

TelegramDownloader/Data/db/IDbService.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,17 @@ public interface IDbService
6767
Task MarkTaskAsError(string internalId, string errorMessage);
6868
Task CleanupStaleTasks(int maxAgeDays = 7);
6969
Task ClearAllTasks();
70+
71+
// Maintenance operations
72+
Task<List<string>> GetAllChannelDatabaseNames();
73+
Task<DatabaseStats> GetDatabaseStats(string dbName);
74+
}
75+
76+
public class DatabaseStats
77+
{
78+
public long SizeInBytes { get; set; }
79+
public long DocumentCount { get; set; }
80+
public DateTime? CreatedAt { get; set; }
81+
public DateTime? LastModified { get; set; }
7082
}
7183
}

TelegramDownloader/Pages/Config.razor

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
@inject ToastService toastService
1111
@inject TransactionInfoService tis
1212
@inject IHostApplicationLifetime applicationLifetime
13+
@using TelegramDownloader.Shared
1314

1415
<style>
1516
.config-page {
@@ -702,6 +703,33 @@
702703
</div>
703704
</div>
704705

706+
<!-- Maintenance Section -->
707+
<div class="config-section">
708+
<div class="config-section-header">
709+
<i class="bi bi-tools"></i>
710+
<span>Maintenance</span>
711+
</div>
712+
<div class="config-section-body">
713+
<div class="config-item">
714+
<div class="config-item-info">
715+
<div class="config-item-label">
716+
<i class="bi bi-database-gear"></i>
717+
Database Cleanup
718+
</div>
719+
<div class="config-item-description">
720+
Scan for orphaned databases from channels you no longer have access to. These may be from channels you left or that were deleted.
721+
</div>
722+
</div>
723+
<div class="config-item-control">
724+
<button type="button" class="btn btn-outline-warning btn-sm d-flex align-items-center gap-2" @onclick="OpenDatabaseMaintenance">
725+
<i class="bi bi-search"></i>
726+
<span>Scan</span>
727+
</button>
728+
</div>
729+
</div>
730+
</div>
731+
</div>
732+
705733
<!-- System Section -->
706734
<div class="config-section">
707735
<div class="config-section-header">
@@ -865,6 +893,11 @@
865893
applicationLifetime.StopApplication();
866894
}
867895

896+
private void OpenDatabaseMaintenance()
897+
{
898+
MainLayout.OpenDatabaseMaintenanceModal();
899+
}
900+
868901
private void RefreshImageCacheInfo()
869902
{
870903
try

0 commit comments

Comments
 (0)