22using DocumentGenerator . Core . DTOs ;
33using DocumentGenerator . Core . Entities ;
44using DocumentGenerator . Core . Interfaces ;
5+ using DocumentGenerator . Core . Settings ;
56using DocumentGenerator . Infrastructure . Data ;
67using HandlebarsDotNet ;
78using Microsoft . EntityFrameworkCore ;
89using Microsoft . Extensions . Logging ;
10+ using Microsoft . Extensions . Options ;
911using System . Text . Json ;
1012
1113namespace DocumentGenerator . Infrastructure . Services
@@ -22,13 +24,19 @@ public DocumentService(
2224 ApplicationDbContext context ,
2325 IPdfService pdfService ,
2426 IMapper mapper ,
25- ILogger < DocumentService > logger )
27+ ILogger < DocumentService > logger ,
28+ IOptions < StorageSettings > storageSettings )
2629 {
2730 _context = context ;
2831 _pdfService = pdfService ;
2932 _mapper = mapper ;
3033 _logger = logger ;
31- _storagePath = Path . Combine ( Directory . GetCurrentDirectory ( ) , "GeneratedDocuments" ) ;
34+
35+ var documentsPath = storageSettings . Value . DocumentsPath ;
36+ _storagePath = Path . IsPathRooted ( documentsPath )
37+ ? documentsPath
38+ : Path . Combine ( Directory . GetCurrentDirectory ( ) , documentsPath ) ;
39+
3240 if ( ! Directory . Exists ( _storagePath ) )
3341 {
3442 Directory . CreateDirectory ( _storagePath ) ;
@@ -37,10 +45,11 @@ public DocumentService(
3745
3846 public async Task < DocumentDto > GenerateDocumentAsync ( GenerationRequestDto request , Guid userId )
3947 {
40- var template = await _context . Templates . FindAsync ( request . TemplateId ) ;
48+ var template = await _context . Templates
49+ . FirstOrDefaultAsync ( t => t . Id == request . TemplateId && t . CreatedByUserId == userId ) ;
4150 if ( template == null )
4251 {
43- _logger . LogWarning ( "Template {TemplateId} not found for document generation " , request . TemplateId ) ;
52+ _logger . LogWarning ( "Template {TemplateId} not found or not owned by user {UserId} " , request . TemplateId , userId ) ;
4453 throw new KeyNotFoundException ( "Template not found" ) ;
4554 }
4655
@@ -87,29 +96,41 @@ public async Task<BatchGenerationResultDto> GenerateDocumentBatchAsync(BatchGene
8796 TotalRequested = request . DataItems . Count
8897 } ;
8998
90- var template = await _context . Templates . FindAsync ( request . TemplateId ) ;
99+ var template = await _context . Templates
100+ . FirstOrDefaultAsync ( t => t . Id == request . TemplateId && t . CreatedByUserId == userId ) ;
91101 if ( template == null )
92102 {
93- _logger . LogWarning ( "Template {TemplateId} not found for batch document generation" , request . TemplateId ) ;
103+ _logger . LogWarning ( "Template {TemplateId} not found or not owned by user {UserId} for batch generation" , request . TemplateId , userId ) ;
94104 throw new KeyNotFoundException ( "Template not found" ) ;
95105 }
96106
97107 _logger . LogInformation ( "Starting batch generation of {Count} documents from template {TemplateId} for user {UserId}" ,
98108 request . DataItems . Count , request . TemplateId , userId ) ;
99109
100110 var compiledTemplate = Handlebars . Compile ( template . Content ) ;
111+ var generatedFiles = new List < string > ( ) ;
101112
102- for ( int i = 0 ; i < request . DataItems . Count ; i ++ )
113+ await using var transaction = await _context . Database . BeginTransactionAsync ( ) ;
114+ try
103115 {
104- try
116+ for ( int i = 0 ; i < request . DataItems . Count ; i ++ )
105117 {
106118 var data = request . DataItems [ i ] ;
107119 var htmlContent = compiledTemplate ( data ) ;
108120 var pdfBytes = await _pdfService . GeneratePdfAsync ( htmlContent ) ;
109121
110122 var fileName = $ "doc-{ Guid . NewGuid ( ) } .pdf";
111123 var filePath = Path . Combine ( _storagePath , fileName ) ;
112- await File . WriteAllBytesAsync ( filePath , pdfBytes ) ;
124+
125+ try
126+ {
127+ await File . WriteAllBytesAsync ( filePath , pdfBytes ) ;
128+ generatedFiles . Add ( filePath ) ;
129+ }
130+ catch ( IOException ex )
131+ {
132+ throw new InvalidOperationException ( $ "Failed to save document file at index { i } ", ex ) ;
133+ }
113134
114135 var document = new Document
115136 {
@@ -130,24 +151,27 @@ public async Task<BatchGenerationResultDto> GenerateDocumentBatchAsync(BatchGene
130151 result . Documents . Add ( dto ) ;
131152 result . SuccessCount ++ ;
132153 }
133- catch ( Exception ex )
134- {
135- _logger . LogWarning ( ex , "Failed to generate document at index {Index} in batch" , i ) ;
136- result . Errors . Add ( new BatchGenerationError
137- {
138- Index = i ,
139- Message = ex . Message
140- } ) ;
141- result . FailureCount ++ ;
142- }
154+
155+ await _context . SaveChangesAsync ( ) ;
156+ await transaction . CommitAsync ( ) ;
157+
158+ _logger . LogInformation ( "Batch generation completed successfully: {Count} documents generated" , result . SuccessCount ) ;
159+ return result ;
143160 }
161+ catch ( Exception ex )
162+ {
163+ _logger . LogError ( ex , "Batch generation failed, rolling back {Count} files" , generatedFiles . Count ) ;
144164
145- await _context . SaveChangesAsync ( ) ;
165+ await transaction . RollbackAsync ( ) ;
146166
147- _logger . LogInformation ( "Batch generation completed: {Success} succeeded, {Failed} failed" ,
148- result . SuccessCount , result . FailureCount ) ;
167+ // Clean up any files that were written
168+ foreach ( var file in generatedFiles )
169+ {
170+ DeleteFileIfExists ( file ) ;
171+ }
149172
150- return result ;
173+ throw ;
174+ }
151175 }
152176
153177 public async Task < DocumentDto ? > GetByIdAsync ( Guid id , Guid userId )
@@ -195,11 +219,26 @@ public async Task<PaginatedResult<DocumentDto>> GetAllAsync(Guid userId, int pag
195219 var document = await _context . Documents . FindAsync ( id ) ;
196220 if ( document == null || document . UserId != userId ) return null ;
197221
222+ // Validate path is within storage directory (path traversal protection)
223+ if ( ! IsPathSafe ( document . StoragePath ) )
224+ {
225+ _logger . LogWarning ( "Attempted path traversal detected for document {DocumentId}" , id ) ;
226+ throw new UnauthorizedAccessException ( "Invalid document path" ) ;
227+ }
228+
198229 if ( ! File . Exists ( document . StoragePath ) )
199230 throw new FileNotFoundException ( "Document file not found on server" ) ;
200231
201- var bytes = await File . ReadAllBytesAsync ( document . StoragePath ) ;
202- return ( bytes , Path . GetFileName ( document . StoragePath ) ) ;
232+ try
233+ {
234+ var bytes = await File . ReadAllBytesAsync ( document . StoragePath ) ;
235+ return ( bytes , Path . GetFileName ( document . StoragePath ) ) ;
236+ }
237+ catch ( IOException ex )
238+ {
239+ _logger . LogError ( ex , "Failed to read document file {DocumentId}" , id ) ;
240+ throw new InvalidOperationException ( "Failed to read document file" , ex ) ;
241+ }
203242 }
204243
205244 public async Task < bool > DeleteAsync ( Guid id , Guid userId )
@@ -211,11 +250,15 @@ public async Task<bool> DeleteAsync(Guid id, Guid userId)
211250 return false ;
212251 }
213252
214- DeleteFileIfExists ( document . StoragePath ) ;
253+ var storagePath = document . StoragePath ;
215254
255+ // Delete from database first to prevent race conditions
216256 _context . Documents . Remove ( document ) ;
217257 await _context . SaveChangesAsync ( ) ;
218258
259+ // Then delete the file (after DB commit)
260+ DeleteFileIfExists ( storagePath ) ;
261+
219262 _logger . LogInformation ( "Document {DocumentId} deleted by user {UserId}" , id , userId ) ;
220263 return true ;
221264 }
@@ -226,14 +269,22 @@ public async Task DeleteByTemplateIdAsync(Guid templateId)
226269 . Where ( d => d . TemplateId == templateId )
227270 . ToListAsync ( ) ;
228271
272+ // Collect file paths before removing from context
273+ var filePaths = documents . Select ( d => d . StoragePath ) . ToList ( ) ;
274+
229275 foreach ( var doc in documents )
230276 {
231- DeleteFileIfExists ( doc . StoragePath ) ;
232277 _context . Documents . Remove ( doc ) ;
233278 }
234279
235280 await _context . SaveChangesAsync ( ) ;
236281
282+ // Delete files after DB commit
283+ foreach ( var path in filePaths )
284+ {
285+ DeleteFileIfExists ( path ) ;
286+ }
287+
237288 _logger . LogInformation ( "Deleted {Count} documents for template {TemplateId}" , documents . Count , templateId ) ;
238289 }
239290
@@ -251,5 +302,19 @@ private void DeleteFileIfExists(string path)
251302 }
252303 }
253304 }
305+
306+ private bool IsPathSafe ( string filePath )
307+ {
308+ try
309+ {
310+ var fullPath = Path . GetFullPath ( filePath ) ;
311+ var storagePath = Path . GetFullPath ( _storagePath ) ;
312+ return fullPath . StartsWith ( storagePath , StringComparison . OrdinalIgnoreCase ) ;
313+ }
314+ catch
315+ {
316+ return false ;
317+ }
318+ }
254319 }
255320}
0 commit comments