22using DfE . CoreLibs . FileStorage . Settings ;
33using DfE . CoreLibs . FileStorage . Exceptions ;
44using System . IO ;
5+ using System . Text . RegularExpressions ;
56using FileNotFoundException = DfE . CoreLibs . FileStorage . Exceptions . FileNotFoundException ;
7+ using System . Xml . Linq ;
68
79namespace DfE . CoreLibs . FileStorage . Services ;
810
@@ -16,6 +18,9 @@ public class LocalFileStorageService : IFileStorageService
1618 private readonly bool _allowOverwrite ;
1719 private readonly long _maxFileSizeBytes ;
1820 private readonly string [ ] _allowedExtensions ;
21+ private readonly Regex ? _allowedFileNamePattern ;
22+ private readonly string _friendlyAllowedFileNamePattern ;
23+ private readonly string _friendlyAllowedFileExtensionsPattern ;
1924
2025 /// <summary>
2126 /// Creates a new instance of the service using the provided configuration <paramref name="options"/>.
@@ -28,10 +33,25 @@ public LocalFileStorageService(FileStorageOptions options)
2833 ArgumentNullException . ThrowIfNull ( options ) ;
2934
3035 var localOptions = options . Local ;
36+ _friendlyAllowedFileNamePattern = localOptions . AllowedFileNamePatternFriendlyList ;
37+ _friendlyAllowedFileExtensionsPattern = localOptions . AllowedExtensionsFriendlyList ;
3138 _createDirectoryIfNotExists = localOptions . CreateDirectoryIfNotExists ;
3239 _allowOverwrite = localOptions . AllowOverwrite ;
3340 _maxFileSizeBytes = localOptions . MaxFileSizeBytes ;
3441 _allowedExtensions = localOptions . AllowedExtensions ?? Array . Empty < string > ( ) ;
42+
43+ // Initialize filename pattern if specified
44+ if ( ! string . IsNullOrWhiteSpace ( localOptions . AllowedFileNamePattern ) )
45+ {
46+ try
47+ {
48+ _allowedFileNamePattern = new Regex ( localOptions . AllowedFileNamePattern , RegexOptions . Compiled ) ;
49+ }
50+ catch ( ArgumentException ex )
51+ {
52+ throw new FileStorageConfigurationException ( $ "Invalid filename pattern: { localOptions . AllowedFileNamePattern } ", ex ) ;
53+ }
54+ }
3555
3656 // Determine base directory
3757 if ( string . IsNullOrWhiteSpace ( localOptions . BaseDirectory ) )
@@ -66,13 +86,35 @@ public LocalFileStorageService(FileStorageOptions options)
6686 /// <summary>
6787 /// Internal constructor used for testing with custom settings.
6888 /// </summary>
69- internal LocalFileStorageService ( string baseDirectory , bool createDirectoryIfNotExists = true , bool allowOverwrite = true , long maxFileSizeBytes = 100 * 1024 * 1024 , string [ ] allowedExtensions = null )
89+ internal LocalFileStorageService ( string baseDirectory ,
90+ bool createDirectoryIfNotExists = true ,
91+ bool allowOverwrite = true ,
92+ long maxFileSizeBytes = 100 * 1024 * 1024 ,
93+ string [ ] allowedExtensions = null ,
94+ string allowedFileNamePattern = null ,
95+ string ? friendlyAllowedFileNamePattern = "a-z A-Z 0-9 _ - no-space" ,
96+ string ? friendlyAllowedFileExtensionsPattern = "\" jpg\" , \" png\" , \" pdf\" , \" docx\" " )
7097 {
7198 _baseDirectory = Path . GetFullPath ( baseDirectory ) ;
99+ _friendlyAllowedFileNamePattern = friendlyAllowedFileNamePattern ;
100+ _friendlyAllowedFileExtensionsPattern = friendlyAllowedFileExtensionsPattern ;
72101 _createDirectoryIfNotExists = createDirectoryIfNotExists ;
73102 _allowOverwrite = allowOverwrite ;
74103 _maxFileSizeBytes = maxFileSizeBytes ;
75104 _allowedExtensions = allowedExtensions ?? Array . Empty < string > ( ) ;
105+
106+ // Initialize filename pattern if specified
107+ if ( ! string . IsNullOrWhiteSpace ( allowedFileNamePattern ) )
108+ {
109+ try
110+ {
111+ _allowedFileNamePattern = new Regex ( allowedFileNamePattern , RegexOptions . Compiled ) ;
112+ }
113+ catch ( ArgumentException ex )
114+ {
115+ throw new FileStorageConfigurationException ( $ "Invalid filename pattern: { allowedFileNamePattern } ", ex ) ;
116+ }
117+ }
76118
77119 if ( _createDirectoryIfNotExists && ! Directory . Exists ( _baseDirectory ) )
78120 {
@@ -81,7 +123,7 @@ internal LocalFileStorageService(string baseDirectory, bool createDirectoryIfNot
81123 }
82124
83125 /// <inheritdoc />
84- public async Task UploadAsync ( string path , Stream content , CancellationToken token = default )
126+ public async Task UploadAsync ( string path , Stream content , string ? originalFileName = null , CancellationToken token = default )
85127 {
86128 ArgumentException . ThrowIfNullOrWhiteSpace ( path ) ;
87129 ArgumentNullException . ThrowIfNull ( content ) ;
@@ -99,13 +141,28 @@ public async Task UploadAsync(string path, Stream content, CancellationToken tok
99141 var fileExtension = Path . GetExtension ( path ) ;
100142 if ( string . IsNullOrEmpty ( fileExtension ) )
101143 {
102- throw new FileStorageException ( $ "File extension is required. Allowed extensions: { string . Join ( ", " , _allowedExtensions ) } ") ;
144+ throw new FileStorageException ( $ "File extension is required. Allowed extensions: { _friendlyAllowedFileExtensionsPattern } ") ;
103145 }
104146
105147 var extensionWithoutDot = fileExtension . TrimStart ( '.' ) ;
106148 if ( ! _allowedExtensions . Contains ( extensionWithoutDot , StringComparer . OrdinalIgnoreCase ) )
107149 {
108- throw new FileStorageException ( $ "File extension '{ extensionWithoutDot } ' is not allowed. Allowed extensions: { string . Join ( ", " , _allowedExtensions ) } ") ;
150+ throw new FileStorageException ( $ "File extension '{ extensionWithoutDot } ' is not allowed. Allowed extensions: { _friendlyAllowedFileExtensionsPattern } ") ;
151+ }
152+ }
153+
154+ // Validate filename pattern if configured
155+ if ( _allowedFileNamePattern != null && originalFileName != null )
156+ {
157+ var fileName = Path . GetFileNameWithoutExtension ( originalFileName ) ;
158+ if ( string . IsNullOrEmpty ( fileName ) )
159+ {
160+ throw new FileStorageException ( $ "Filename is required when filename pattern validation is enabled. Pattern: { _friendlyAllowedFileNamePattern } ") ;
161+ }
162+
163+ if ( ! _allowedFileNamePattern . IsMatch ( fileName ) )
164+ {
165+ throw new FileStorageException ( $ "Filename '{ fileName } ' does not match the allowed pattern. Pattern: { _friendlyAllowedFileNamePattern } ") ;
109166 }
110167 }
111168
0 commit comments