Skip to content

Commit 09d24c7

Browse files
committed
feat: add task persistence
1 parent 9dcd990 commit 09d24c7

21 files changed

Lines changed: 2725 additions & 102 deletions

TelegramDownloader/Data/FileService.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,18 +232,20 @@ public class FileService : IFileService
232232
protected ILogger<IFileService> _logger { get; set; }
233233
protected TransactionInfoService _tis { get; set; }
234234
protected ToastService _toastService { get; set; }
235+
protected ITaskPersistenceService _persistence { get; set; }
235236
private static Mutex refreshMutex = new Mutex();
236237

237-
const int MaxSize = 1024 * 1024 * 1000; // 1GB
238+
const int MaxSize = 1024 * 1024 * 1000; // 1GB
238239

239240

240-
public FileService(ITelegramService ts, IDbService db, ILogger<IFileService> logger, TransactionInfoService tis, ToastService toastService)
241+
public FileService(ITelegramService ts, IDbService db, ILogger<IFileService> logger, TransactionInfoService tis, ToastService toastService, ITaskPersistenceService persistence)
241242
{
242243
_ts = ts;
243244
_db = db;
244245
_tis = tis;
245246
_toastService = toastService;
246247
_logger = logger;
248+
_persistence = persistence;
247249
createTempFolder();
248250
}
249251

@@ -1227,6 +1229,18 @@ public async Task AddUploadFileFromServer(string dbName, string currentPath, Lis
12271229
}
12281230
idt.callbacks = new Callbacks();
12291231
idt.callbacks.callback = async () => await UploadFileFromServer(dbName, currentPath, files, idt);
1232+
1233+
// Persist the batch task
1234+
try
1235+
{
1236+
idt.channelId = dbName;
1237+
await _persistence.PersistBatchTask(idt);
1238+
}
1239+
catch (Exception ex)
1240+
{
1241+
_logger.LogWarning(ex, "Failed to persist batch task - continuing without persistence");
1242+
}
1243+
12301244
_tis.addToInfoDownloadTaskList(idt);
12311245
_tis.CheckPendingUploadInfoTasks();
12321246
}

TelegramDownloader/Data/FileServiceV2.cs

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public FileServiceV2(
2727
IDbService db,
2828
ILogger<IFileService> logger,
2929
TransactionInfoService tis,
30-
ToastService toastService
31-
) : base(ts, db, logger, tis, toastService)
30+
ToastService toastService,
31+
ITaskPersistenceService persistence
32+
) : base(ts, db, logger, tis, toastService, persistence)
3233
{
33-
3434
}
3535

3636
public override async Task downloadFile(string dbName, List<FileManagerDirectoryContent> files, string targetPath, string? collectionId = null, string? channelId = null)
@@ -133,16 +133,44 @@ public async Task downloadFromTelegramV2(string dbName, int messageId, string de
133133
{
134134
model.channelName = "Public or Shared";
135135
}
136+
137+
// Setup persistence
138+
model.OnProgressPersist = async (transmitted, progress, state) =>
139+
{
140+
try
141+
{
142+
await _persistence.UpdateProgress(model._internalId, transmitted, progress, state);
143+
}
144+
catch { }
145+
};
146+
147+
// Persist download task
148+
try
149+
{
150+
await _persistence.PersistDownload(model, dbName, messageId, file);
151+
}
152+
catch (Exception ex)
153+
{
154+
_logger.LogWarning(ex, "Failed to persist download task - continuing without persistence");
155+
}
156+
136157
var tcs = new TaskCompletionSource<bool>();
137158

138159
EventHandler handler = null;
139-
handler = (sender, args) =>
160+
handler = async (sender, args) =>
140161
{
141162
if (model.state == StateTask.Completed)
142163
{
143164
model.EventStatechanged -= handler;
165+
await _persistence.MarkCompleted(model._internalId);
144166
tcs.SetResult(true);
145167
}
168+
else if (model.state == StateTask.Error || model.state == StateTask.Canceled)
169+
{
170+
model.EventStatechanged -= handler;
171+
await _persistence.MarkError(model._internalId, model.state.ToString());
172+
tcs.TrySetResult(false);
173+
}
146174
};
147175

148176
model.EventStatechanged += handler;
@@ -173,6 +201,46 @@ public async Task DownloadFileNowV2(string dbName, int messageId, string destPat
173201

174202
}
175203

204+
/// <summary>
205+
/// Download file with support for resuming from a specific byte offset
206+
/// </summary>
207+
public async Task DownloadFileNowV2WithOffset(string dbName, int messageId, string destPath, DownloadModel model, long resumeOffset = 0)
208+
{
209+
TL.Message m = await _ts.getMessageFile(dbName, messageId);
210+
ChatMessages cm = new ChatMessages();
211+
cm.message = m;
212+
model.startDate = DateTime.Now;
213+
214+
cm.user = null;
215+
cm.isDocument = false;
216+
if (m.media is MessageMediaDocument { document: Document document })
217+
{
218+
cm.isDocument = true;
219+
model._size = document.size;
220+
model._sizeString = Services.HelperService.SizeSuffix(document.size);
221+
}
222+
223+
// Determine file mode based on resume offset
224+
FileMode fileMode = resumeOffset > 0 ? FileMode.Append : FileMode.Create;
225+
226+
_logger.LogInformation("DownloadFileNowV2WithOffset - File: {Name}, Offset: {Offset}, Mode: {Mode}",
227+
model.name, resumeOffset, fileMode);
228+
229+
using (FileStream fs = new FileStream(destPath, fileMode, FileAccess.Write, FileShare.ReadWrite))
230+
{
231+
// If resuming, validate file size
232+
if (resumeOffset > 0 && fs.Length < resumeOffset)
233+
{
234+
// File is smaller than expected offset - restart from current file size
235+
resumeOffset = fs.Length;
236+
model._transmitted = resumeOffset;
237+
model._transmittedString = Services.HelperService.SizeSuffix(resumeOffset);
238+
}
239+
240+
await _ts.DownloadFileAndReturnWithOffset(cm, ms: fs, model: model, offset: resumeOffset);
241+
}
242+
}
243+
176244
public override async Task<int> PreloadFilesToTemp(string channelId, List<FileManagerDirectoryContent> items)
177245
{
178246
_logger.LogInformation("Starting preload files to temp V2 - ChannelId: {ChannelId}, ItemsCount: {Count}", channelId, items.Count);

TelegramDownloader/Data/ITelegramService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public interface ITelegramService
1919
Task<string> DownloadFile(ChatMessages message, string fileName = null, string folder = null, DownloadModel model = null, bool shouldAddToList = false);
2020
Task<Byte[]> DownloadFileStream(Message message, long offset, int limit);
2121
Task<Stream> DownloadFileAndReturn(ChatMessages message, Stream ms = null, string fileName = null, string folder = null, DownloadModel model = null);
22+
Task<Stream> DownloadFileAndReturnWithOffset(ChatMessages message, Stream ms = null, string fileName = null, string folder = null, DownloadModel model = null, long offset = 0);
2223
Task<List<ChatViewBase>> GetFouriteChannels(bool mustRefresh = true);
2324
Task AddFavouriteChannel(long id);
2425
Task RemoveFavouriteChannel(long id);

TelegramDownloader/Data/TelegramService.cs

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,20 @@ public class TelegramService : ITelegramService
2626
private const int FILESPLITSIZE = 524288; // 512 * 1024;
2727
private TransactionInfoService _tis { get; set; }
2828
private IDbService _db { get; set; }
29+
private ITaskPersistenceService _persistence { get; set; }
2930
private static WTelegram.Client client = null;
3031
private static Messages_Chats chats = null;
3132
private static List<ChatViewBase> favouriteChannels = new List<ChatViewBase>();
3233
private static Mutex mut = new Mutex();
3334
private ILogger<IFileService> _logger { get; set; }
3435

3536

36-
public TelegramService(TransactionInfoService tis, IDbService db, ILogger<IFileService> logger)
37+
public TelegramService(TransactionInfoService tis, IDbService db, ILogger<IFileService> logger, ITaskPersistenceService persistence)
3738
{
3839
_tis = tis;
3940
_db = db;
4041
_logger = logger;
42+
_persistence = persistence;
4143
// createDownloadFolder();
4244
mut.WaitOne();
4345
if (client == null)
@@ -341,16 +343,39 @@ public async Task<Message> uploadFile(string chatId, Stream file, string fileNam
341343
um.startDate = DateTime.Now;
342344
um._transmitted = 0;
343345
_tis.addToUploadList(um);
346+
347+
// Persist the upload task
348+
try
349+
{
350+
um.OnProgressPersist = async (transmitted, progress, state) =>
351+
{
352+
await _persistence.UpdateProgress(um._internalId, transmitted, progress, state);
353+
};
354+
await _persistence.PersistUpload(um, chatId, um.path);
355+
}
356+
catch (Exception ex)
357+
{
358+
_logger.LogWarning(ex, "Failed to persist upload task - continuing without persistence");
359+
}
360+
344361
try
345362
{
346363
var inputFile = await client.UploadFileAsync(file, fileName, um.ProgressCallback);
347364
var result = await client.SendMediaAsync(peer, caption ?? fileName, inputFile, mimeType);
348365
_logger.LogInformation("File upload completed - FileName: {FileName}, MessageId: {MessageId}", fileName, result.id);
366+
367+
// Mark task as completed
368+
await _persistence.MarkCompleted(um._internalId);
369+
349370
return result;
350371
}
351372
catch (Exception ex)
352373
{
353374
_logger.LogError(ex, "File upload failed - FileName: {FileName}, ChatId: {ChatId}", fileName, chatId);
375+
376+
// Mark task as error
377+
await _persistence.MarkError(um._internalId, ex.Message);
378+
354379
throw;
355380
}
356381
}
@@ -803,6 +828,129 @@ public async Task<Stream> DownloadFileAndReturn(ChatMessages message, Stream ms
803828
return null;
804829
}
805830

831+
public async Task<Stream> DownloadFileAndReturnWithOffset(ChatMessages message, Stream ms = null, string fileName = null, string folder = null, DownloadModel model = null, long offset = 0)
832+
{
833+
if (model == null)
834+
{
835+
model = new DownloadModel();
836+
model.tis = _tis;
837+
}
838+
839+
model.m = message;
840+
model.channel = message.user;
841+
842+
if (message.message.media is MessageMediaDocument { document: TL.Document document })
843+
{
844+
model._size = document.size;
845+
model._transmitted = offset;
846+
var filename = fileName ?? document.Filename;
847+
filename ??= $"{document.id}.{document.mime_type[(document.mime_type.IndexOf('/') + 1)..]}";
848+
if (model.name == null)
849+
model.name = filename;
850+
851+
_logger.LogInformation("Starting document download with offset - FileName: {FileName}, Size: {SizeMB:F2}MB, Offset: {Offset}",
852+
filename, document.size / (1024.0 * 1024.0), offset);
853+
854+
if (offset == 0)
855+
{
856+
// No offset - use standard download
857+
MemoryStream dest = new MemoryStream();
858+
await client.DownloadFileAsync(document, ms ?? dest, null, model.ProgressCallback);
859+
_logger.LogInformation("Document download completed - FileName: {FileName}", filename);
860+
return ms ?? dest;
861+
}
862+
863+
// Offset-based download using Upload_GetFile API
864+
Stream targetStream = ms ?? new MemoryStream();
865+
long currentOffset = offset;
866+
long totalSize = document.size;
867+
868+
InputDocument inputFile = document;
869+
var location = new InputDocumentFileLocation
870+
{
871+
id = inputFile.id,
872+
access_hash = inputFile.access_hash,
873+
file_reference = inputFile.file_reference,
874+
thumb_size = ""
875+
};
876+
877+
_logger.LogInformation("Resuming download from offset {Offset} of {TotalSize} bytes", currentOffset, totalSize);
878+
879+
while (currentOffset < totalSize)
880+
{
881+
int chunkSize = FILESPLITSIZE; // 512KB chunks
882+
if (currentOffset + chunkSize > totalSize)
883+
{
884+
// Adjust last chunk size (must be multiple of 4096 for Telegram API)
885+
long remaining = totalSize - currentOffset;
886+
chunkSize = (int)((remaining + 4095) / 4096 * 4096); // Round up to 4096
887+
if (chunkSize > FILESPLITSIZE)
888+
chunkSize = FILESPLITSIZE;
889+
}
890+
891+
Upload_FileBase file = null;
892+
try
893+
{
894+
file = await client.Upload_GetFile(location, currentOffset, limit: chunkSize);
895+
}
896+
catch (RpcException ex) when (ex.Code == 303 && ex.Message == "FILE_MIGRATE_X")
897+
{
898+
_logger.LogWarning("DC migration required, switching to DC {DC}", -ex.X);
899+
client = await client.GetClientForDC(-ex.X, true);
900+
file = await client.Upload_GetFile(location, currentOffset, limit: chunkSize);
901+
}
902+
catch (RpcException ex) when (ex.Code == 400 && ex.Message == "OFFSET_INVALID")
903+
{
904+
_logger.LogError(ex, "OFFSET_INVALID at offset {Offset} - file may have changed", currentOffset);
905+
throw;
906+
}
907+
catch (Exception ex)
908+
{
909+
_logger.LogError(ex, "Download error at offset {Offset}", currentOffset);
910+
throw;
911+
}
912+
913+
if (file is Upload_File uploadFile)
914+
{
915+
var fileBytes = uploadFile.bytes;
916+
if (fileBytes.Length == 0)
917+
{
918+
_logger.LogInformation("Received empty chunk - download complete at offset {Offset}", currentOffset);
919+
break;
920+
}
921+
922+
// Write to stream - for append mode, the stream position should already be at the end
923+
await targetStream.WriteAsync(fileBytes, 0, fileBytes.Length);
924+
currentOffset += fileBytes.Length;
925+
model._transmitted = currentOffset;
926+
927+
// Call progress callback
928+
model.ProgressCallback(currentOffset, totalSize);
929+
930+
continue;
931+
}
932+
933+
throw new InvalidOperationException("Unexpected file type returned from Telegram API.");
934+
}
935+
936+
_logger.LogInformation("Document download with offset completed - FileName: {FileName}, FinalOffset: {Offset}",
937+
filename, currentOffset);
938+
return targetStream;
939+
}
940+
else if (message.message.media is MessageMediaPhoto { photo: Photo photo })
941+
{
942+
// Photos don't support resume - they're small enough to re-download
943+
_logger.LogInformation("Photo download (no resume support) - PhotoId: {PhotoId}", photo.id);
944+
var filename = $"{photo.id}.jpg";
945+
MemoryStream dest = new MemoryStream();
946+
var type = await client.DownloadFileAsync(photo, ms ?? dest, (PhotoSizeBase)null, model.ProgressCallback);
947+
dest.Close();
948+
return ms ?? dest;
949+
}
950+
951+
return null;
952+
}
953+
806954
public async Task<string> DownloadFile(ChatMessages message, string fileName = null, string folder = null, DownloadModel model = null, bool shouldAddToList = false)
807955
{
808956
if (model == null)

0 commit comments

Comments
 (0)