├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ └── pr.yml ├── .gitignore ├── .issuetracker ├── Directory.Build.props ├── LICENSE.md ├── README.md ├── ReleaseNotes.md ├── StrongName.snk ├── System.IO.Abstractions.Extensions.sln ├── global.json ├── images └── icon_256x256.png ├── renovate.json ├── src ├── Directory.Build.props └── System.IO.Abstractions.Extensions │ ├── DisposableDirectory.cs │ ├── DisposableFile.cs │ ├── DisposableFileSystemInfo.cs │ ├── IDirectoryInfoExtensions.cs │ ├── IFileInfoAsyncExtensions.cs │ ├── IFileInfoExtensions.cs │ ├── IFileSystemExtensions.cs │ ├── LineEnumerator.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── StringResources.cs │ └── System.IO.Abstractions.Extensions.csproj ├── tests ├── Directory.Build.props └── System.IO.Abstractions.Extensions.Tests │ ├── DirectoryInfoExtensionsTests.cs │ ├── DisposableDirectoryTests.cs │ ├── DisposableFileTests.cs │ ├── FileInfoExtensionsTests.cs │ ├── FileSystemExtensionsTests.cs │ └── System.IO.Abstractions.Extensions.Tests.csproj └── version.json /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "C# (.NET Core)", 3 | "image": "mcr.microsoft.com/vscode/devcontainers/dotnet:9.0", 4 | "settings": { 5 | "terminal.integrated.shell.linux": "/bin/bash" 6 | }, 7 | "postCreateCommand": "dotnet restore" 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://EditorConfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = CRLF 8 | 9 | [*.cs] 10 | indent_style = space 11 | indent_size = 4 12 | 13 | dotnet_diagnostic.NUnit2003.severity = suggestion 14 | dotnet_diagnostic.NUnit2004.severity = suggestion 15 | dotnet_diagnostic.NUnit2005.severity = suggestion -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | # Force bash scripts to always use lf line endings so that if a repo is accessed 66 | # in Unix via a file share from Windows, the scripts will work. 67 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'state: needs discussion, type: bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'state: needs discussion, type: enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - 'feature/**' 7 | pull_request: 8 | branches: [main] 9 | jobs: 10 | test: 11 | name: Test 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout sources 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v4 24 | with: 25 | dotnet-version: | 26 | 8.0.x 27 | 9.0.x 28 | - name: Run tests 29 | run: dotnet test --collect:"XPlat Code Coverage" --logger "GitHubActions" 30 | - name: Upload coverage 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: Code coverage ${{ matrix.os }} 34 | path: "**/coverage.cobertura.xml" 35 | coverage: 36 | name: Coverage 37 | needs: [test] 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout sources 41 | uses: actions/checkout@v4 42 | - name: Setup .NET 43 | uses: actions/setup-dotnet@v4 44 | - uses: actions/download-artifact@v4 45 | with: 46 | name: Code coverage ubuntu-latest 47 | path: coverage-ubuntu 48 | - uses: actions/download-artifact@v4 49 | with: 50 | name: Code coverage windows-latest 51 | path: coverage-windows 52 | - uses: actions/download-artifact@v4 53 | with: 54 | name: Code coverage macos-latest 55 | path: coverage-macos 56 | - name: Generate coverage report 57 | uses: danielpalme/ReportGenerator-GitHub-Action@5.2.2 58 | with: 59 | reports: "**/coverage.cobertura.xml" 60 | targetdir: "coverage-report" 61 | reporttypes: "Cobertura" 62 | - name: Publish coverage report to Codacy 63 | uses: codacy/codacy-coverage-reporter-action@master 64 | if: github.repository == 'System-IO-Abstractions/System.IO.Abstractions.Extensions' && github.event_name == 'push' 65 | with: 66 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 67 | coverage-reports: coverage-report/Cobertura.xml 68 | pack: 69 | name: Pack 70 | needs: [test] 71 | runs-on: ubuntu-latest 72 | steps: 73 | - name: Checkout sources 74 | uses: actions/checkout@v4 75 | with: 76 | fetch-depth: 0 77 | - name: Setup .NET 78 | uses: actions/setup-dotnet@v4 79 | - name: Create packages 80 | run: dotnet pack --configuration Release --output ./packages 81 | - name: Upload a Build Artifact 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: NuGet packages 85 | path: packages/*.* 86 | deploy: 87 | name: Deploy 88 | if: github.ref == 'refs/heads/main' && github.event_name == 'push' 89 | needs: [pack] 90 | runs-on: ubuntu-latest 91 | steps: 92 | - name: Checkout sources 93 | uses: actions/checkout@v4 94 | with: 95 | fetch-depth: 0 96 | - name: Setup .NET 97 | uses: actions/setup-dotnet@v4 98 | - uses: actions/download-artifact@v4 99 | with: 100 | name: NuGet packages 101 | path: packages 102 | - name: Push packages 103 | run: dotnet nuget push "packages/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json 104 | - uses: dotnet/nbgv@v0.4.2 105 | id: nbgv 106 | - name: Create GitHub release 107 | uses: actions/create-release@v1 108 | env: 109 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | with: 111 | tag_name: v${{ steps.nbgv.outputs.SemVer2 }} 112 | release_name: v${{ steps.nbgv.outputs.SemVer2 }} 113 | - name: Comment relevant issues and merge requests 114 | uses: apexskier/github-release-commenter@v1.3.6 115 | with: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | comment-template: | 118 | This is addressed in release {release_link}. 119 | label-template: | 120 | state: released 121 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request" 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | jobs: 9 | main: 10 | name: Check PR title 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: amannn/action-semantic-pull-request@v5.4.0 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=csharp,visualstudio,visualstudiocode 4 | 5 | ### Csharp ### 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 10 | 11 | # User-specific files 12 | *.rsuser 13 | *.suo 14 | *.user 15 | *.userosscache 16 | *.sln.docstates 17 | 18 | # User-specific files (MonoDevelop/Xamarin Studio) 19 | *.userprefs 20 | 21 | # Mono auto generated files 22 | mono_crash.* 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Dd]ebugPublic/ 27 | [Rr]elease/ 28 | [Rr]eleases/ 29 | x64/ 30 | x86/ 31 | [Ww][Ii][Nn]32/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | [Ll]ogs/ 39 | 40 | # Visual Studio 2015/2017 cache/options directory 41 | .vs/ 42 | # Uncomment if you have tasks that create the project's static files in wwwroot 43 | #wwwroot/ 44 | 45 | # Visual Studio 2017 auto generated files 46 | Generated\ Files/ 47 | 48 | # MSTest test Results 49 | [Tt]est[Rr]esult*/ 50 | [Bb]uild[Ll]og.* 51 | 52 | # NUnit 53 | *.VisualState.xml 54 | TestResult.xml 55 | nunit-*.xml 56 | 57 | # Build Results of an ATL Project 58 | [Dd]ebugPS/ 59 | [Rr]eleasePS/ 60 | dlldata.c 61 | 62 | # Benchmark Results 63 | BenchmarkDotNet.Artifacts/ 64 | 65 | # .NET Core 66 | project.lock.json 67 | project.fragment.lock.json 68 | artifacts/ 69 | 70 | # ASP.NET Scaffolding 71 | ScaffoldingReadMe.txt 72 | 73 | # StyleCop 74 | StyleCopReport.xml 75 | 76 | # Files built by Visual Studio 77 | *_i.c 78 | *_p.c 79 | *_h.h 80 | *.ilk 81 | *.meta 82 | *.obj 83 | *.iobj 84 | *.pch 85 | *.pdb 86 | *.ipdb 87 | *.pgc 88 | *.pgd 89 | *.rsp 90 | *.sbr 91 | *.tlb 92 | *.tli 93 | *.tlh 94 | *.tmp 95 | *.tmp_proj 96 | *_wpftmp.csproj 97 | *.log 98 | *.tlog 99 | *.vspscc 100 | *.vssscc 101 | .builds 102 | *.pidb 103 | *.svclog 104 | *.scc 105 | 106 | # Chutzpah Test files 107 | _Chutzpah* 108 | 109 | # Visual C++ cache files 110 | ipch/ 111 | *.aps 112 | *.ncb 113 | *.opendb 114 | *.opensdf 115 | *.sdf 116 | *.cachefile 117 | *.VC.db 118 | *.VC.VC.opendb 119 | 120 | # Visual Studio profiler 121 | *.psess 122 | *.vsp 123 | *.vspx 124 | *.sap 125 | 126 | # Visual Studio Trace Files 127 | *.e2e 128 | 129 | # TFS 2012 Local Workspace 130 | $tf/ 131 | 132 | # Guidance Automation Toolkit 133 | *.gpState 134 | 135 | # ReSharper is a .NET coding add-in 136 | _ReSharper*/ 137 | *.[Rr]e[Ss]harper 138 | *.DotSettings.user 139 | 140 | # TeamCity is a build add-in 141 | _TeamCity* 142 | 143 | # DotCover is a Code Coverage Tool 144 | *.dotCover 145 | 146 | # AxoCover is a Code Coverage Tool 147 | .axoCover/* 148 | !.axoCover/settings.json 149 | 150 | # Coverlet is a free, cross platform Code Coverage Tool 151 | coverage*.json 152 | coverage*.xml 153 | coverage*.info 154 | 155 | # Visual Studio code coverage results 156 | *.coverage 157 | *.coveragexml 158 | 159 | # NCrunch 160 | _NCrunch_* 161 | .*crunch*.local.xml 162 | nCrunchTemp_* 163 | 164 | # MightyMoose 165 | *.mm.* 166 | AutoTest.Net/ 167 | 168 | # Web workbench (sass) 169 | .sass-cache/ 170 | 171 | # Installshield output folder 172 | [Ee]xpress/ 173 | 174 | # DocProject is a documentation generator add-in 175 | DocProject/buildhelp/ 176 | DocProject/Help/*.HxT 177 | DocProject/Help/*.HxC 178 | DocProject/Help/*.hhc 179 | DocProject/Help/*.hhk 180 | DocProject/Help/*.hhp 181 | DocProject/Help/Html2 182 | DocProject/Help/html 183 | 184 | # Click-Once directory 185 | publish/ 186 | 187 | # Publish Web Output 188 | *.[Pp]ublish.xml 189 | *.azurePubxml 190 | # Note: Comment the next line if you want to checkin your web deploy settings, 191 | # but database connection strings (with potential passwords) will be unencrypted 192 | *.pubxml 193 | *.publishproj 194 | 195 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 196 | # checkin your Azure Web App publish settings, but sensitive information contained 197 | # in these scripts will be unencrypted 198 | PublishScripts/ 199 | 200 | # NuGet Packages 201 | *.nupkg 202 | # NuGet Symbol Packages 203 | *.snupkg 204 | # The packages folder can be ignored because of Package Restore 205 | **/[Pp]ackages/* 206 | # except build/, which is used as an MSBuild target. 207 | !**/[Pp]ackages/build/ 208 | # Uncomment if necessary however generally it will be regenerated when needed 209 | #!**/[Pp]ackages/repositories.config 210 | # NuGet v3's project.json files produces more ignorable files 211 | *.nuget.props 212 | *.nuget.targets 213 | 214 | # Nuget personal access tokens and Credentials 215 | nuget.config 216 | 217 | # Microsoft Azure Build Output 218 | csx/ 219 | *.build.csdef 220 | 221 | # Microsoft Azure Emulator 222 | ecf/ 223 | rcf/ 224 | 225 | # Windows Store app package directories and files 226 | AppPackages/ 227 | BundleArtifacts/ 228 | Package.StoreAssociation.xml 229 | _pkginfo.txt 230 | *.appx 231 | *.appxbundle 232 | *.appxupload 233 | 234 | # Visual Studio cache files 235 | # files ending in .cache can be ignored 236 | *.[Cc]ache 237 | # but keep track of directories ending in .cache 238 | !?*.[Cc]ache/ 239 | 240 | # Others 241 | ClientBin/ 242 | ~$* 243 | *~ 244 | *.dbmdl 245 | *.dbproj.schemaview 246 | *.jfm 247 | *.pfx 248 | *.publishsettings 249 | orleans.codegen.cs 250 | 251 | # Including strong name files can present a security risk 252 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 253 | #*.snk 254 | 255 | # Since there are multiple workflows, uncomment next line to ignore bower_components 256 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 257 | #bower_components/ 258 | 259 | # RIA/Silverlight projects 260 | Generated_Code/ 261 | 262 | # Backup & report files from converting an old project file 263 | # to a newer Visual Studio version. Backup files are not needed, 264 | # because we have git ;-) 265 | _UpgradeReport_Files/ 266 | Backup*/ 267 | UpgradeLog*.XML 268 | UpgradeLog*.htm 269 | ServiceFabricBackup/ 270 | *.rptproj.bak 271 | 272 | # SQL Server files 273 | *.mdf 274 | *.ldf 275 | *.ndf 276 | 277 | # Business Intelligence projects 278 | *.rdl.data 279 | *.bim.layout 280 | *.bim_*.settings 281 | *.rptproj.rsuser 282 | *- [Bb]ackup.rdl 283 | *- [Bb]ackup ([0-9]).rdl 284 | *- [Bb]ackup ([0-9][0-9]).rdl 285 | 286 | # Microsoft Fakes 287 | FakesAssemblies/ 288 | 289 | # GhostDoc plugin setting file 290 | *.GhostDoc.xml 291 | 292 | # Node.js Tools for Visual Studio 293 | .ntvs_analysis.dat 294 | node_modules/ 295 | 296 | # Visual Studio 6 build log 297 | *.plg 298 | 299 | # Visual Studio 6 workspace options file 300 | *.opt 301 | 302 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 303 | *.vbw 304 | 305 | # Visual Studio LightSwitch build output 306 | **/*.HTMLClient/GeneratedArtifacts 307 | **/*.DesktopClient/GeneratedArtifacts 308 | **/*.DesktopClient/ModelManifest.xml 309 | **/*.Server/GeneratedArtifacts 310 | **/*.Server/ModelManifest.xml 311 | _Pvt_Extensions 312 | 313 | # Paket dependency manager 314 | .paket/paket.exe 315 | paket-files/ 316 | 317 | # FAKE - F# Make 318 | .fake/ 319 | 320 | # CodeRush personal settings 321 | .cr/personal 322 | 323 | # Python Tools for Visual Studio (PTVS) 324 | __pycache__/ 325 | *.pyc 326 | 327 | # Cake - Uncomment if you are using it 328 | # tools/** 329 | # !tools/packages.config 330 | 331 | # Tabs Studio 332 | *.tss 333 | 334 | # Telerik's JustMock configuration file 335 | *.jmconfig 336 | 337 | # BizTalk build output 338 | *.btp.cs 339 | *.btm.cs 340 | *.odx.cs 341 | *.xsd.cs 342 | 343 | # OpenCover UI analysis results 344 | OpenCover/ 345 | 346 | # Azure Stream Analytics local run output 347 | ASALocalRun/ 348 | 349 | # MSBuild Binary and Structured Log 350 | *.binlog 351 | 352 | # NVidia Nsight GPU debugger configuration file 353 | *.nvuser 354 | 355 | # MFractors (Xamarin productivity tool) working folder 356 | .mfractor/ 357 | 358 | # Local History for Visual Studio 359 | .localhistory/ 360 | 361 | # BeatPulse healthcheck temp database 362 | healthchecksdb 363 | 364 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 365 | MigrationBackup/ 366 | 367 | # Ionide (cross platform F# VS Code tools) working folder 368 | .ionide/ 369 | 370 | # Fody - auto-generated XML schema 371 | FodyWeavers.xsd 372 | 373 | # VS Code files for those working on multiple tools 374 | .vscode/* 375 | !.vscode/settings.json 376 | !.vscode/tasks.json 377 | !.vscode/launch.json 378 | !.vscode/extensions.json 379 | *.code-workspace 380 | 381 | # Local History for Visual Studio Code 382 | .history/ 383 | 384 | # Windows Installer files from build outputs 385 | *.cab 386 | *.msi 387 | *.msix 388 | *.msm 389 | *.msp 390 | 391 | # JetBrains Rider 392 | .idea/ 393 | *.sln.iml 394 | 395 | ### VisualStudioCode ### 396 | 397 | # Local History for Visual Studio Code 398 | 399 | ### VisualStudioCode Patch ### 400 | # Ignore all local history of files 401 | .history 402 | .ionide 403 | 404 | ### VisualStudio ### 405 | 406 | # User-specific files 407 | 408 | # User-specific files (MonoDevelop/Xamarin Studio) 409 | 410 | # Mono auto generated files 411 | 412 | # Build results 413 | 414 | # Visual Studio 2015/2017 cache/options directory 415 | # Uncomment if you have tasks that create the project's static files in wwwroot 416 | 417 | # Visual Studio 2017 auto generated files 418 | 419 | # MSTest test Results 420 | 421 | # NUnit 422 | 423 | # Build Results of an ATL Project 424 | 425 | # Benchmark Results 426 | 427 | # .NET Core 428 | 429 | # ASP.NET Scaffolding 430 | 431 | # StyleCop 432 | 433 | # Files built by Visual Studio 434 | 435 | # Chutzpah Test files 436 | 437 | # Visual C++ cache files 438 | 439 | # Visual Studio profiler 440 | 441 | # Visual Studio Trace Files 442 | 443 | # TFS 2012 Local Workspace 444 | 445 | # Guidance Automation Toolkit 446 | 447 | # ReSharper is a .NET coding add-in 448 | 449 | # TeamCity is a build add-in 450 | 451 | # DotCover is a Code Coverage Tool 452 | 453 | # AxoCover is a Code Coverage Tool 454 | 455 | # Coverlet is a free, cross platform Code Coverage Tool 456 | 457 | # Visual Studio code coverage results 458 | 459 | # NCrunch 460 | 461 | # MightyMoose 462 | 463 | # Web workbench (sass) 464 | 465 | # Installshield output folder 466 | 467 | # DocProject is a documentation generator add-in 468 | 469 | # Click-Once directory 470 | 471 | # Publish Web Output 472 | # Note: Comment the next line if you want to checkin your web deploy settings, 473 | # but database connection strings (with potential passwords) will be unencrypted 474 | 475 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 476 | # checkin your Azure Web App publish settings, but sensitive information contained 477 | # in these scripts will be unencrypted 478 | 479 | # NuGet Packages 480 | # NuGet Symbol Packages 481 | # The packages folder can be ignored because of Package Restore 482 | # except build/, which is used as an MSBuild target. 483 | # Uncomment if necessary however generally it will be regenerated when needed 484 | # NuGet v3's project.json files produces more ignorable files 485 | 486 | # Nuget personal access tokens and Credentials 487 | 488 | # Microsoft Azure Build Output 489 | 490 | # Microsoft Azure Emulator 491 | 492 | # Windows Store app package directories and files 493 | 494 | # Visual Studio cache files 495 | # files ending in .cache can be ignored 496 | # but keep track of directories ending in .cache 497 | 498 | # Others 499 | 500 | # Including strong name files can present a security risk 501 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 502 | 503 | # Since there are multiple workflows, uncomment next line to ignore bower_components 504 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 505 | 506 | # RIA/Silverlight projects 507 | 508 | # Backup & report files from converting an old project file 509 | # to a newer Visual Studio version. Backup files are not needed, 510 | # because we have git ;-) 511 | 512 | # SQL Server files 513 | 514 | # Business Intelligence projects 515 | 516 | # Microsoft Fakes 517 | 518 | # GhostDoc plugin setting file 519 | 520 | # Node.js Tools for Visual Studio 521 | 522 | # Visual Studio 6 build log 523 | 524 | # Visual Studio 6 workspace options file 525 | 526 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 527 | 528 | # Visual Studio LightSwitch build output 529 | 530 | # Paket dependency manager 531 | 532 | # FAKE - F# Make 533 | 534 | # CodeRush personal settings 535 | 536 | # Python Tools for Visual Studio (PTVS) 537 | 538 | # Cake - Uncomment if you are using it 539 | # tools/** 540 | # !tools/packages.config 541 | 542 | # Tabs Studio 543 | 544 | # Telerik's JustMock configuration file 545 | 546 | # BizTalk build output 547 | 548 | # OpenCover UI analysis results 549 | 550 | # Azure Stream Analytics local run output 551 | 552 | # MSBuild Binary and Structured Log 553 | 554 | # NVidia Nsight GPU debugger configuration file 555 | 556 | # MFractors (Xamarin productivity tool) working folder 557 | 558 | # Local History for Visual Studio 559 | 560 | # BeatPulse healthcheck temp database 561 | 562 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 563 | 564 | # Ionide (cross platform F# VS Code tools) working folder 565 | 566 | # Fody - auto-generated XML schema 567 | 568 | # VS Code files for those working on multiple tools 569 | 570 | # Local History for Visual Studio Code 571 | 572 | # Windows Installer files from build outputs 573 | 574 | # JetBrains Rider 575 | 576 | ### VisualStudio Patch ### 577 | # Additional files built by Visual Studio 578 | 579 | # End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudio,visualstudiocode 580 | *.sh 581 | -------------------------------------------------------------------------------- /.issuetracker: -------------------------------------------------------------------------------- 1 | # Integration with Issue Tracker 2 | # 3 | # (note that '\' need to be escaped). 4 | 5 | [issuetracker "GitHub"] 6 | regex = "#(\\d+)" 7 | url = "https://github.com/System-IO-Abstractions/System.IO.Abstractions.Extensions.git/issues/$1" 8 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | System.IO.Abstractions.Extensions 4 | Copyright © Tatham Oddie - Luigi Grilli & friends 2021 5 | Tatham Oddie - Luigi Grilli & friends 6 | True 7 | $(MSBuildThisFileDirectory)StrongName.snk 8 | true 9 | true 10 | snupkg 11 | true 12 | testing 13 | https://github.com/System-IO-Abstractions/System.IO.Abstractions.Extensions 14 | https://github.com/System-IO-Abstractions/System.IO.Abstractions.Extensions.git 15 | git 16 | $(MSBuildThisFileDirectory) 17 | MIT 18 | README.md 19 | 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers 23 | all 24 | 25 | 26 | 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | all 29 | 30 | 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Tatham Oddie - Luigi Grilli & friends 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![System.IO.Abstractions.Extensions](https://socialify.git.ci/System-IO-Abstractions/System.IO.Abstractions.Extensions/image?description=1&font=Source%20Code%20Pro&forks=1&issues=1&pattern=Charlie%20Brown&pulls=1&stargazers=1&theme=Dark) 2 | [![NuGet](https://img.shields.io/nuget/v/TestableIO.System.IO.Abstractions.Extensions.svg)](https://www.nuget.org/packages/TestableIO.System.IO.Abstractions.Extensions) 3 | ![Continuous Integration](https://github.com/TestableIO/System.IO.Abstractions.Extensions/workflows/Continuous%20Integration/badge.svg) 4 | [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/) 5 | 6 | 7 | 8 | Convenience functionality on top of System.IO.Abstractions 9 | 10 | ```shell 11 | dotnet add package TestableIO.System.IO.Abstractions.Extensions 12 | ``` 13 | 14 | # Examples 15 | 16 | ## CurrentDirectory extension 17 | 18 | ```csharp 19 | var fs = new FileSystem(); 20 | 21 | //with extension 22 | var current = fs.CurrentDirectory(); 23 | 24 | //without extension 25 | var current = fs.DirectoryInfo.FromDirectoryName(fs.Directory.GetCurrentDirectory()); 26 | ``` 27 | 28 | ## SubDirectory extension 29 | 30 | ```csharp 31 | var current = new FileSystem().CurrentDirectory(); 32 | 33 | //create a "temp" subdirectory with extension 34 | current.SubDirectory("temp").Create(); 35 | 36 | //create a "temp" subdirectory without extension 37 | current.FileSystem.DirectoryInfo.FromDirectoryName(current.FileSystem.Path.Combine(current.FullName, "temp")).Create(); 38 | ``` 39 | 40 | ## File extension 41 | 42 | ```csharp 43 | var current = new FileSystem().CurrentDirectory(); 44 | 45 | //create a "test.txt" file with extension 46 | using (var stream = current.File("test.txt").Create()) 47 | stream.Dispose(); 48 | 49 | //create a "test.txt" file without extension 50 | using (var stream = current.FileSystem.FileInfo.FromFileName(current.FileSystem.Path.Combine(current.FullName, "test.txt")).Create()) 51 | stream.Dispose(); 52 | ``` 53 | 54 | ## Automatic cleanup with Disposable extensions 55 | 56 | Use `CreateDisposableDirectory` or `CreateDisposableFile` to create a `IDirectoryInfo` or `IFileInfo` that's automatically 57 | deleted when the returned `IDisposable` is disposed. 58 | 59 | ```csharp 60 | var fs = new FileSystem(); 61 | 62 | //with extension 63 | using (fs.CreateDisposableDirectory(out IDirectoryInfo dir)) 64 | { 65 | Console.WriteLine($"This directory will be deleted when control leaves the using block: '{dir.FullName}'"); 66 | } 67 | 68 | //without extension 69 | var temp = fs.Path.GetTempPath(); 70 | var fileName = fs.Path.GetRandomFileName(); 71 | var path = fs.Path.Combine(temp, fileName); 72 | 73 | try 74 | { 75 | IDirectoryInfo dir = fs.Directory.CreateDirectory(path); 76 | Console.WriteLine($"This directory will be deleted in the finally block: '{dir.FullName}'"); 77 | } 78 | finally 79 | { 80 | fs.Directory.Delete(path, recursive: true); 81 | } 82 | ``` 83 | 84 | ## IDirectoryInfo.CopyTo extension 85 | ```csharp 86 | var fs = new FileSystem(); 87 | var current = fs.CurrentDirectory(); 88 | 89 | //source 90 | var source = current.SubDirectory("source"); 91 | source.Create(); //make sure the source directory exists 92 | 93 | //destination 94 | var dest = current.SubDirectory("destination"); 95 | 96 | //copy 97 | source.CopyTo(dest, recursive: true); 98 | ``` 99 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | List of notable changes between majors 4 | 5 | ## 22.0.0 6 | 7 | - (Breaking) Removed support for some Async calls in .NET Framework and 8 | legacy versions of .NET to better support .NET 8 and later 9 | - Minimum required TestableIO.System.IO.Abstractions version is now 22.x 10 | - Removed .NET 7 build (should still work with .NET standard build) 11 | - Removed .NET 6 build (should still work with .NET standard build) 12 | 13 | ## 2.0.0 14 | 15 | - (Breaking) Moved all extensions methods to 'System.IO.Abstractions' namespace 16 | - Added ThrowIfNotFound extension methods 17 | - Removed .NET 5 build (should still work with .NET standard build) -------------------------------------------------------------------------------- /StrongName.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TestableIO/System.IO.Abstractions.Extensions/5caba455d528f93c6e26039dd0f93d930b1d899b/StrongName.snk -------------------------------------------------------------------------------- /System.IO.Abstractions.Extensions.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.6.33815.320 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E4D67E71-DBAC-41BD-B9FC-477075F82980}" 6 | ProjectSection(SolutionItems) = preProject 7 | src\Directory.Build.props = src\Directory.Build.props 8 | EndProjectSection 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Abstractions.Extensions", "src\System.IO.Abstractions.Extensions\System.IO.Abstractions.Extensions.csproj", "{89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{32B54437-9DA5-42AF-9922-9AE6ABAC30A8}" 13 | ProjectSection(SolutionItems) = preProject 14 | tests\Directory.Build.props = tests\Directory.Build.props 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.IO.Abstractions.Extensions.Tests", "tests\System.IO.Abstractions.Extensions.Tests\System.IO.Abstractions.Extensions.Tests.csproj", "{5145BBFD-BB63-4D70-89DF-0C5B2772752A}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F69100BC-0333-4F19-8981-8F17FD4A1A99}" 20 | ProjectSection(SolutionItems) = preProject 21 | .editorconfig = .editorconfig 22 | Directory.Build.props = Directory.Build.props 23 | global.json = global.json 24 | LICENSE.md = LICENSE.md 25 | README.md = README.md 26 | ReleaseNotes.md = ReleaseNotes.md 27 | version.json = version.json 28 | EndProjectSection 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Debug|x64 = Debug|x64 34 | Debug|x86 = Debug|x86 35 | Release|Any CPU = Release|Any CPU 36 | Release|x64 = Release|x64 37 | Release|x86 = Release|x86 38 | EndGlobalSection 39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 40 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|x64.ActiveCfg = Debug|Any CPU 43 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|x64.Build.0 = Debug|Any CPU 44 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Debug|x86.Build.0 = Debug|Any CPU 46 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|x64.ActiveCfg = Release|Any CPU 49 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|x64.Build.0 = Release|Any CPU 50 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|x86.ActiveCfg = Release|Any CPU 51 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9}.Release|x86.Build.0 = Release|Any CPU 52 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|x64.ActiveCfg = Debug|Any CPU 55 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|x64.Build.0 = Debug|Any CPU 56 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|x86.ActiveCfg = Debug|Any CPU 57 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Debug|x86.Build.0 = Debug|Any CPU 58 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|x64.ActiveCfg = Release|Any CPU 61 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|x64.Build.0 = Release|Any CPU 62 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|x86.ActiveCfg = Release|Any CPU 63 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A}.Release|x86.Build.0 = Release|Any CPU 64 | EndGlobalSection 65 | GlobalSection(SolutionProperties) = preSolution 66 | HideSolutionNode = FALSE 67 | EndGlobalSection 68 | GlobalSection(NestedProjects) = preSolution 69 | {89D9E244-E25D-40AA-A31E-8EDA1EBFDFD9} = {E4D67E71-DBAC-41BD-B9FC-477075F82980} 70 | {5145BBFD-BB63-4D70-89DF-0C5B2772752A} = {32B54437-9DA5-42AF-9922-9AE6ABAC30A8} 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {C4FB0DBE-5DAA-4E40-B91A-B37C57893528} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0", 4 | "rollForward": "latestMinor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /images/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TestableIO/System.IO.Abstractions.Extensions/5caba455d528f93c6e26039dd0f93d930b1d899b/images/icon_256x256.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | snupkg 7 | true 8 | icon_256x256.png 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/DisposableDirectory.cs: -------------------------------------------------------------------------------- 1 | namespace System.IO.Abstractions 2 | { 3 | /// 4 | /// Creates a class that wraps a . That wrapped directory will be 5 | /// deleted when the method is called. 6 | /// 7 | /// 8 | public class DisposableDirectory : DisposableFileSystemInfo 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// 14 | /// The directory to delete when this object is disposed. 15 | /// 16 | public DisposableDirectory(IDirectoryInfo directoryInfo) : base(directoryInfo) 17 | { 18 | } 19 | 20 | /// 21 | protected override void DeleteFileSystemInfo() 22 | { 23 | fileSystemInfo.Delete(recursive: true); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/DisposableFile.cs: -------------------------------------------------------------------------------- 1 | namespace System.IO.Abstractions 2 | { 3 | /// 4 | /// Creates a class that wraps a . That wrapped file will be 5 | /// deleted when the method is called. 6 | /// 7 | /// 8 | public class DisposableFile : DisposableFileSystemInfo 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// 14 | /// The file to delete when this object is disposed. 15 | /// 16 | public DisposableFile(IFileInfo fileInfo) : base(fileInfo) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/DisposableFileSystemInfo.cs: -------------------------------------------------------------------------------- 1 | namespace System.IO.Abstractions 2 | { 3 | /// 4 | /// Creates a class that wraps a . That wrapped object will be deleted 5 | /// when the method is called. 6 | /// 7 | /// 8 | /// This class is designed for the using pattern to ensure that a directory is 9 | /// created and then deleted when it is no longer referenced. This is sometimes called 10 | /// the RAII pattern (see https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization). 11 | /// 12 | public class DisposableFileSystemInfo : IDisposable where T : IFileSystemInfo 13 | { 14 | protected T fileSystemInfo; 15 | private bool isDisposed; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// 21 | /// The directory to delete when this object is disposed. 22 | /// 23 | public DisposableFileSystemInfo(T fileSystemInfo) 24 | { 25 | this.fileSystemInfo = fileSystemInfo ?? throw new ArgumentNullException(nameof(fileSystemInfo)); 26 | 27 | // Do an attribute refresh so that the object we return to the caller 28 | // has up-to-date properties (like Exists). 29 | this.fileSystemInfo.Refresh(); 30 | } 31 | 32 | /// 33 | /// Performs the actual work of releasing resources. This allows for subclasses to participate 34 | /// in resource release. 35 | /// 36 | /// 37 | /// true if if the call comes from a method, false if it 38 | /// comes from a finalizer. 39 | /// 40 | protected virtual void Dispose(bool disposing) 41 | { 42 | if (!isDisposed) 43 | { 44 | if (disposing) 45 | { 46 | DeleteFileSystemInfo(); 47 | 48 | // Do an attribute refresh so that the object we returned to the 49 | // caller has up-to-date properties (like Exists). 50 | fileSystemInfo.Refresh(); 51 | } 52 | 53 | isDisposed = true; 54 | } 55 | } 56 | 57 | /// 58 | public void Dispose() 59 | { 60 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method. 61 | // See https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose. 62 | Dispose(disposing: true); 63 | GC.SuppressFinalize(this); 64 | } 65 | 66 | /// 67 | /// Deletes the wrapped object. 68 | /// 69 | /// 70 | /// Different types of objects have different ways of deleting themselves (e.g. 71 | /// directories usually need to be deleted recursively). This method is called by the 72 | /// 73 | protected virtual void DeleteFileSystemInfo() 74 | { 75 | fileSystemInfo.Delete(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/IDirectoryInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace System.IO.Abstractions 5 | { 6 | public static class IDirectoryInfoExtensions 7 | { 8 | /// 9 | /// Get an for the specified sub-directory 10 | /// 11 | /// 12 | /// Sub-directory name (ex. "test") 13 | /// An for the specified sub-directory 14 | public static IDirectoryInfo SubDirectory(this IDirectoryInfo info, string name) 15 | { 16 | return info.FileSystem.DirectoryInfo.New(info.FileSystem.Path.Combine(info.FullName, name)); 17 | } 18 | 19 | /// 20 | /// Get an for the specified sub-directories 21 | /// 22 | /// 23 | /// Sub-directory names (ex. "test", "test2"). Empty or null names are automatically removed from this list 24 | /// An for the specified sub-directory 25 | public static IDirectoryInfo SubDirectory(this IDirectoryInfo info, params string[] names) 26 | { 27 | return info.SubDirectory((IEnumerable)names); 28 | } 29 | 30 | /// 31 | /// Get an for the specified sub-directories 32 | /// 33 | /// 34 | /// Sub-directory names (ex. "test", "test2"). Empty or null names are automatically removed from this list 35 | /// An for the specified sub-directory 36 | public static IDirectoryInfo SubDirectory(this IDirectoryInfo info, IEnumerable names) 37 | { 38 | return info.FileSystem.DirectoryInfo.New(info.FileSystem.Path.Combine(GetPaths(info, names))); 39 | } 40 | 41 | /// 42 | /// Get an for the specified file 43 | /// 44 | /// 45 | /// File name (ex. "test.txt") 46 | /// An for the specified file 47 | public static IFileInfo File(this IDirectoryInfo info, string name) 48 | { 49 | return info.FileSystem.FileInfo.New(info.FileSystem.Path.Combine(info.FullName, name)); 50 | } 51 | 52 | /// 53 | /// Get the full path for the specified file in the folder 54 | /// 55 | /// 56 | /// File name (ex. "test.txt") 57 | /// A with the full path of the file 58 | public static string GetFilePath(this IDirectoryInfo info, string name) 59 | { 60 | return info.FileSystem.Path.Combine(info.FullName, name); 61 | } 62 | 63 | /// 64 | /// Get an for the specified sub-directories file 65 | /// 66 | /// 67 | /// Sub-directories and file name (ex. "test", "test.txt"). Empty or null names are automatically removed from this list 68 | /// An for the specified file 69 | public static IFileInfo File(this IDirectoryInfo info, params string[] names) 70 | { 71 | return info.File((IEnumerable)names); 72 | } 73 | 74 | /// 75 | /// Get an for the specified sub-directories file 76 | /// 77 | /// 78 | /// Sub-directories and file name (ex. "test", "test.txt"). Empty or null names are automatically removed from this list 79 | /// An for the specified file 80 | public static IFileInfo File(this IDirectoryInfo info, IEnumerable names) 81 | { 82 | return info.FileSystem.FileInfo.New(info.FileSystem.Path.Combine(GetPaths(info, names))); 83 | } 84 | 85 | /// 86 | /// Throws an exception if the directory doesn't exists 87 | /// 88 | /// Directory that will be checked for existance 89 | /// Exception thrown if the directory is not found 90 | public static void ThrowIfNotFound(this IDirectoryInfo info) 91 | { 92 | if (!info.Exists) 93 | { 94 | throw new DirectoryNotFoundException(StringResources.Format("COULD_NOT_FIND_PART_OF_PATH_EXCEPTION", info.FullName)); 95 | } 96 | } 97 | 98 | /// 99 | /// Checks if is an ancestor of . 100 | /// If is a parent this method will return 101 | /// If and are the same directory, this will return 102 | /// 103 | /// Ancestor directory 104 | /// Child directory (sub-directory) 105 | /// True if is an ancestor of otherwise false 106 | public static bool IsAncestorOf(this IDirectoryInfo ancestor, IDirectoryInfo child) 107 | { 108 | return child.FullName.Length > ancestor.FullName.Length && 109 | child.FullName.StartsWith(ancestor.FullName); 110 | } 111 | 112 | /// 113 | /// Checks if is an ancestor of and returns 114 | /// a list of segments of the paths of that are not in common with 115 | /// 116 | /// Ancestor directory 117 | /// Child directory (sub-directory) 118 | /// A with the segments of the paths of not in common with 119 | /// Exception thrown if is not an ancestor of 120 | public static string[] DiffPaths(this IDirectoryInfo ancestor, IDirectoryInfo child) 121 | { 122 | if (!ancestor.IsAncestorOf(child)) 123 | { 124 | throw new ArgumentException(StringResources.Format("NOT_AN_ANCESTOR", ancestor.FullName, child.FullName), nameof(child)); 125 | } 126 | 127 | return child.FullName.Substring(ancestor.FullName.Length + 1) 128 | .Split(ancestor.FileSystem.Path.PathSeparator); 129 | } 130 | 131 | /// 132 | /// Applies a between and . 133 | /// The resulting diff of path segments is applied to and returned. 134 | /// If the flag is set to true the resulting subdirectory of will also be created. 135 | /// must be the same directory or an ancestor of otherwise this method will throw an 136 | /// 137 | /// Ancestor directory 138 | /// Child directory (sub-directory) 139 | /// Directory to apply the diff to 140 | /// If set to true, the resulting directory will also be created 141 | /// An which is either a child of or ifself 142 | public static IDirectoryInfo TranslatePaths( 143 | this IDirectoryInfo ancestor1, 144 | IDirectoryInfo child, 145 | IDirectoryInfo ancestor2, 146 | bool create = false) 147 | { 148 | var ret = ancestor1.Equals(child) 149 | ? ancestor2 150 | : ancestor2.SubDirectory(ancestor1.DiffPaths(child)); 151 | 152 | if (create) 153 | { 154 | ret.Create(); 155 | } 156 | 157 | return ret; 158 | } 159 | 160 | /// 161 | /// Executes a for each file in the directory 162 | /// A action is also executed before entering any directory, including 163 | /// The returned by is passed as parameter into 164 | /// 165 | /// Directory where to search for files 166 | /// Action to apply for each file found in 167 | /// Action to apply upon entering any directory including 168 | /// If true the search will be recursive and will include subfolders of . Defaults to false 169 | /// Search pattern to apply when searching files, defaults to '*' 170 | /// Search pattern to apply when searching directories, defaults to '*' 171 | public static void ForEachFile( 172 | this IDirectoryInfo info, Action fileAction, 173 | Func directoryAction, 174 | bool recursive = false, 175 | string filesSearchPattern = "*", 176 | string directoriesSearchPattern = "*") 177 | { 178 | info.ThrowIfNotFound(); 179 | 180 | var d = directoryAction?.Invoke(info) ?? info; 181 | foreach (var file in info.EnumerateFiles(filesSearchPattern)) 182 | { 183 | fileAction.Invoke(file, d); 184 | } 185 | 186 | if (!recursive) 187 | { 188 | return; 189 | } 190 | 191 | foreach (var dir in info.EnumerateDirectories(directoriesSearchPattern)) 192 | { 193 | dir.ForEachFile(fileAction, directoryAction, recursive, filesSearchPattern, directoriesSearchPattern); 194 | } 195 | } 196 | 197 | /// 198 | /// Copies files from to 199 | /// 200 | /// Source directory 201 | /// Destination directory 202 | /// If true the copy will be recursive and will include subfolders of . Defaults to false 203 | /// If true the copy will overwrite any existing files in . Defaults to false 204 | /// Search pattern to apply when searching files, defaults to '*' 205 | /// Search pattern to apply when searching directories, defaults to '*' 206 | public static void CopyTo( 207 | this IDirectoryInfo source, 208 | IDirectoryInfo destination, 209 | bool recursive = false, 210 | string filesSearchPattern = "*", 211 | string directoriesSearchPattern = "*", 212 | bool overwrite = false) 213 | { 214 | source.ForEachFile( 215 | (file, destDir) => file.CopyTo(destDir.GetFilePath(file.Name), overwrite), 216 | subDirectory => source.TranslatePaths(subDirectory, destination, true), 217 | recursive, 218 | filesSearchPattern, 219 | directoriesSearchPattern); 220 | } 221 | 222 | private static string[] GetPaths(IDirectoryInfo info, IEnumerable names) 223 | { 224 | return new[] { info.FullName } 225 | .Concat(names.Where(n => !String.IsNullOrEmpty(n))) 226 | .ToArray(); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/IFileInfoAsyncExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NET8_0_OR_GREATER 2 | #nullable enable 3 | 4 | using System.Collections.Generic; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace System.IO.Abstractions 11 | { 12 | public static class FileInfoFileAsyncExtensions 13 | { 14 | /// 15 | public static async Task AppendAllLinesAsync(this IFileInfo file, IEnumerable contents, CancellationToken cancellationToken = default) 16 | { 17 | await file.FileSystem.File.AppendAllLinesAsync(file.FullName, contents, cancellationToken); 18 | } 19 | 20 | /// 21 | public static async Task AppendAllTextAsync(this IFileInfo file, string? contents, CancellationToken cancellationToken = default) 22 | { 23 | await file.FileSystem.File.AppendAllTextAsync(file.FullName, contents, cancellationToken); 24 | } 25 | 26 | /// 27 | public static async Task ReadAllBytesAsync(this IFileInfo file, CancellationToken cancellationToken = default) 28 | { 29 | return await file.FileSystem.File.ReadAllBytesAsync(file.FullName, cancellationToken); 30 | } 31 | 32 | /// 33 | public static async Task ReadAllLinesAsync(this IFileInfo file, CancellationToken cancellationToken = default) 34 | { 35 | return await file.FileSystem.File.ReadAllLinesAsync(file.FullName, cancellationToken); 36 | } 37 | 38 | /// 39 | public static async Task ReadAllTextAsync(this IFileInfo file, CancellationToken cancellationToken = default) 40 | { 41 | return await file.FileSystem.File.ReadAllTextAsync(file.FullName, cancellationToken); 42 | } 43 | 44 | /// 45 | public static async Task WriteAllBytesAsync(this IFileInfo file, byte[] bytes, CancellationToken cancellationToken = default) 46 | { 47 | await file.FileSystem.File.WriteAllBytesAsync(file.FullName, bytes, cancellationToken); 48 | } 49 | 50 | /// 51 | public static async Task WriteAllLinesAsync(this IFileInfo file, IEnumerable contents, CancellationToken cancellationToken = default) 52 | { 53 | await file.FileSystem.File.WriteAllLinesAsync(file.FullName, contents, cancellationToken); 54 | } 55 | 56 | /// 57 | public static async Task WriteAllTextAsync(this IFileInfo file, string? contents, CancellationToken cancellationToken = default) 58 | { 59 | await file.FileSystem.File.WriteAllTextAsync(file.FullName, contents, cancellationToken); 60 | } 61 | 62 | /// 63 | /// Creates an that can enumerate asynchronously the lines of text in the specified 64 | /// 65 | /// File to enumerate content 66 | /// 67 | /// Returns an to enumerate the content of the file 68 | public static IAsyncEnumerable EnumerateLinesAsync(this IFileInfo file, CancellationToken cancellationToken) 69 | { 70 | return EnumerateLinesAsync(file, null, cancellationToken); 71 | } 72 | 73 | /// 74 | /// Creates an that can enumerate asynchronously the lines of text in the specified 75 | /// using the specified 76 | /// 77 | /// File to enumerate content 78 | /// Encoding to use to read the file 79 | /// 80 | /// Returns an to enumerate the content of the file 81 | public static async IAsyncEnumerable EnumerateLinesAsync(this IFileInfo file, Encoding? encoding, [EnumeratorCancellation] CancellationToken cancellationToken) 82 | { 83 | await using var stream = file.OpenRead(); 84 | using var reader = encoding == null 85 | ? new StreamReader(stream) 86 | : new StreamReader(stream, encoding); 87 | 88 | var line = await reader.ReadLineAsync(cancellationToken); 89 | while (line != null) 90 | { 91 | yield return line; 92 | cancellationToken.ThrowIfCancellationRequested(); 93 | line = await reader.ReadLineAsync(cancellationToken); 94 | } 95 | } 96 | } 97 | } 98 | #endif -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/IFileInfoExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace System.IO.Abstractions 5 | { 6 | public static class IFileInfoExtensions 7 | { 8 | /// 9 | /// Throws an exception if the doesn't exists 10 | /// 11 | /// File that will be checked for existance 12 | /// Exception thrown if the file is not found 13 | public static void ThrowIfNotFound(this IFileInfo file) 14 | { 15 | if (!file.Exists) 16 | throw new FileNotFoundException(StringResources.Format("COULD_NOT_FIND_FILE_EXCEPTION", file.FullName)); 17 | } 18 | 19 | /// 20 | /// Creates an that can enumerate the lines of text in the 21 | /// 22 | /// File to enumerate content 23 | /// Returns an to enumerate the content of the file 24 | public static IEnumerable EnumerateLines(this IFileInfo file) 25 | { 26 | return new LineEnumerable(file, null); 27 | } 28 | 29 | /// 30 | /// Creates an that can enumerate the lines of text in the specified 31 | /// using the specified 32 | /// 33 | /// File to enumerate content 34 | /// Encoding to use to read the file 35 | /// Returns an to enumerate the content of the file 36 | public static IEnumerable EnumerateLines(this IFileInfo file, Encoding encoding) 37 | { 38 | return new LineEnumerable(file, encoding); 39 | } 40 | 41 | /// 42 | /// Opens a for the in the specified 43 | /// 44 | /// File to open stream on 45 | /// Mode to use when opening the file 46 | /// A that can read or write data to the specified 47 | public static FileSystemStream OpenFileStream(this IFileInfo file, FileMode mode) 48 | { 49 | return file.FileSystem.FileStream.New(file.FullName, mode); 50 | } 51 | 52 | /// 53 | /// Creates a new empty . 54 | /// If the file already exists, the file is truncated. 55 | /// 56 | /// File to create 57 | /// The original so that methods calls can be chained 58 | public static IFileInfo Truncate(this IFileInfo file) 59 | { 60 | using(var stream = file.OpenFileStream(FileMode.Create)) 61 | { 62 | stream.Dispose(); 63 | } 64 | 65 | return file; 66 | } 67 | 68 | /// 69 | /// Writes the specified to the specified using the UTF-8 encoding. 70 | /// If the file already exists and the flag is set to true, the file will be truncated. 71 | /// 72 | /// File to write to 73 | /// Lines to write to file as text 74 | /// Flag that specifies if the file can be overwritten if it exists 75 | /// Exception thrown if the file already exists and the flag is set to 76 | public static void WriteLines(this IFileInfo file, IEnumerable lines, bool overwrite = false) 77 | { 78 | using (var stream = file.OpenFileStream(GetWriteFileMode(file, overwrite))) 79 | using (var writer = new StreamWriter(stream)) 80 | foreach(var line in lines) 81 | { 82 | writer.WriteLine(line); 83 | } 84 | } 85 | 86 | /// 87 | /// Writes the specified to the specified 88 | /// using the specified . 89 | /// If the file already exists and the flag is set to true, the file will be truncated. 90 | /// 91 | /// File to write to 92 | /// Lines to write to file as text 93 | /// Encoding to use when writing the to the text file 94 | /// Flag that specifies if the file can be overwritten if it exists 95 | /// Exception thrown if the file already exists and the flag is set to 96 | public static void WriteLines(this IFileInfo file, IEnumerable lines, Encoding encoding, bool overwrite = false) 97 | { 98 | using (var stream = file.OpenFileStream(GetWriteFileMode(file, overwrite))) 99 | using (var writer = new StreamWriter(stream, encoding)) 100 | foreach (var line in lines) 101 | { 102 | writer.WriteLine(line); 103 | } 104 | } 105 | 106 | /// 107 | /// Appends the specified to the specified 108 | /// 109 | /// File to append to 110 | /// Lines to append to file as text 111 | public static void AppendLines(this IFileInfo file, IEnumerable lines) 112 | { 113 | using (var writer = file.AppendText()) 114 | foreach (var line in lines) 115 | { 116 | writer.WriteLine(line); 117 | } 118 | } 119 | 120 | /// 121 | public static void AppendAllLines(this IFileInfo file, IEnumerable contents) 122 | { 123 | file.FileSystem.File.AppendAllLines(file.FullName, contents); 124 | } 125 | 126 | /// 127 | public static void AppendAllText(this IFileInfo file, string contents) 128 | { 129 | file.FileSystem.File.AppendAllText(file.FullName, contents); 130 | } 131 | 132 | /// 133 | public static byte[] ReadAllBytes(this IFileInfo file) 134 | { 135 | return file.FileSystem.File.ReadAllBytes(file.FullName); 136 | } 137 | 138 | /// 139 | public static string[] ReadAllLines(this IFileInfo file) 140 | { 141 | return file.FileSystem.File.ReadAllLines(file.FullName); 142 | } 143 | 144 | /// 145 | public static string ReadAllText(this IFileInfo file) 146 | { 147 | return file.FileSystem.File.ReadAllText(file.FullName); 148 | } 149 | 150 | /// 151 | public static void WriteAllBytes(this IFileInfo file, byte[] bytes) 152 | { 153 | file.FileSystem.File.WriteAllBytes(file.FullName, bytes); 154 | } 155 | 156 | /// 157 | public static void WriteAllLines(this IFileInfo file, IEnumerable contents) 158 | { 159 | file.FileSystem.File.WriteAllLines(file.FullName, contents); 160 | } 161 | 162 | /// 163 | public static void WriteAllText(this IFileInfo file, string contents) 164 | { 165 | file.FileSystem.File.WriteAllText(file.FullName, contents); 166 | } 167 | 168 | private static FileMode GetWriteFileMode(IFileInfo info, bool overwrite) 169 | { 170 | if (!overwrite && info.Exists) 171 | { 172 | throw new IOException(StringResources.Format("CANNOT_OVERWRITE", info.FullName)); 173 | } 174 | 175 | //if the file already exists it will be truncated 176 | return FileMode.Create; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/IFileSystemExtensions.cs: -------------------------------------------------------------------------------- 1 | // S3874: "out" and "ref" parameters should not be used 2 | // https://rules.sonarsource.com/csharp/RSPEC-3874/ 3 | // 4 | // Our CreateDisposableDirectory / CreateDisposableFile extensions 5 | // intentionally use an out param so that the DirectoryInfo / FileInfo 6 | // is passed out (similar to a Try* method) to the caller while the 7 | // returned object implements IDisposable to leverage the using statement. 8 | #pragma warning disable S3874 9 | 10 | namespace System.IO.Abstractions 11 | { 12 | public static class IFileSystemExtensions 13 | { 14 | /// 15 | /// Get the current directory for the specified 16 | /// 17 | /// FileSystem in use 18 | /// An for the current directory 19 | public static IDirectoryInfo CurrentDirectory(this IFileSystem fileSystem) 20 | { 21 | return fileSystem.DirectoryInfo.New(fileSystem.Directory.GetCurrentDirectory()); 22 | } 23 | 24 | /// 25 | /// Creates a new using a random name from the temp path, and returns an 26 | /// that deletes the directory when disposed. 27 | /// 28 | /// 29 | /// The in use. 30 | /// 31 | /// 32 | /// The for the directory that was created. 33 | /// 34 | /// 35 | /// An to manage the directory's lifetime. 36 | /// 37 | public static IDisposable CreateDisposableDirectory(this IFileSystem fileSystem, out IDirectoryInfo directoryInfo) 38 | { 39 | return fileSystem.CreateDisposableDirectory(fileSystem.Path.GetRandomTempPath(), out directoryInfo); 40 | } 41 | 42 | /// 43 | /// 44 | /// Creates a new using a path provided by , and returns an 45 | /// that deletes the directory when disposed. 46 | /// 47 | /// 48 | /// The full path to the directory to create. 49 | /// 50 | /// 51 | /// If the directory already exists. 52 | /// 53 | public static IDisposable CreateDisposableDirectory(this IFileSystem fileSystem, string path, out IDirectoryInfo directoryInfo) 54 | { 55 | return fileSystem.CreateDisposableDirectory(path, dir => new DisposableDirectory(dir), out directoryInfo); 56 | } 57 | 58 | /// 59 | /// 60 | /// Creates a new using a random name from the temp path and returns an 61 | /// created by , that should delete the directory when disposed. 62 | /// 63 | /// 64 | /// A that acts as a factory method. Given the , create the 65 | /// that will manage the its lifetime. 66 | /// 67 | public static T CreateDisposableDirectory( 68 | this IFileSystem fileSystem, 69 | Func disposableFactory, 70 | out IDirectoryInfo directoryInfo) where T : IDisposable 71 | { 72 | return fileSystem.CreateDisposableDirectory(fileSystem.Path.GetRandomTempPath(), disposableFactory, out directoryInfo); 73 | } 74 | 75 | /// 76 | /// 77 | /// Creates a new using a path provided by , and returns an 78 | /// created by , that should delete the directory when disposed. 79 | /// 80 | /// 81 | /// A that acts as a factory method. Given the , create the 82 | /// that will manage the its lifetime. 83 | /// 84 | public static T CreateDisposableDirectory( 85 | this IFileSystem fileSystem, 86 | string path, 87 | Func disposableFactory, 88 | out IDirectoryInfo directoryInfo) where T : IDisposable 89 | { 90 | directoryInfo = fileSystem.DirectoryInfo.New(path); 91 | 92 | if (directoryInfo.Exists) 93 | { 94 | throw CreateAlreadyExistsException(nameof(path), path); 95 | } 96 | 97 | directoryInfo.Create(); 98 | 99 | return disposableFactory(directoryInfo); 100 | } 101 | 102 | /// 103 | /// Creates a new using a random name from the temp path, and returns an 104 | /// that deletes the file when disposed. 105 | /// 106 | /// 107 | /// The in use. 108 | /// 109 | /// 110 | /// The for the file that was created. 111 | /// 112 | /// 113 | /// An to manage the file's lifetime. 114 | /// 115 | public static IDisposable CreateDisposableFile(this IFileSystem fileSystem, out IFileInfo fileInfo) 116 | { 117 | return fileSystem.CreateDisposableFile(fileSystem.Path.GetRandomTempPath(), out fileInfo); 118 | } 119 | 120 | /// 121 | /// 122 | /// Creates a new using a path provided by , and returns an 123 | /// that deletes the file when disposed. 124 | /// 125 | /// 126 | /// The full path to the file to create. 127 | /// 128 | /// 129 | /// If the file already exists. 130 | /// 131 | public static IDisposable CreateDisposableFile(this IFileSystem fileSystem, string path, out IFileInfo fileInfo) 132 | { 133 | return fileSystem.CreateDisposableFile(path, file => new DisposableFile(file), out fileInfo); 134 | } 135 | 136 | /// 137 | /// 138 | /// Creates a new using a random name from the temp path and returns an 139 | /// created by , that should delete the file when disposed. 140 | /// 141 | /// 142 | /// A that acts as a factory method. Given the , create the 143 | /// that will manage the its lifetime. 144 | /// 145 | public static T CreateDisposableFile( 146 | this IFileSystem fileSystem, 147 | Func disposableFactory, 148 | out IFileInfo fileInfo) where T : IDisposable 149 | { 150 | return fileSystem.CreateDisposableFile(fileSystem.Path.GetRandomTempPath(), disposableFactory, out fileInfo); 151 | } 152 | 153 | /// 154 | /// 155 | /// Creates a new using a path provided by , and returns an 156 | /// created by , that should delete the file when disposed. 157 | /// 158 | /// 159 | /// A that acts as a factory method. Given the , create the 160 | /// that will manage the its lifetime. 161 | /// 162 | public static T CreateDisposableFile( 163 | this IFileSystem fileSystem, 164 | string path, 165 | Func disposableFactory, 166 | out IFileInfo fileInfo) where T : IDisposable 167 | { 168 | fileInfo = fileSystem.FileInfo.New(path); 169 | 170 | if (fileInfo.Exists) 171 | { 172 | throw CreateAlreadyExistsException(nameof(path), path); 173 | } 174 | 175 | // Ensure we close the handle to the file after we create it, otherwise 176 | // callers may get an access denied error. 177 | fileInfo.Create().Dispose(); 178 | 179 | return disposableFactory(fileInfo); 180 | } 181 | 182 | private static string GetRandomTempPath(this IPath path) 183 | { 184 | var temp = path.GetTempPath(); 185 | var fileName = path.GetRandomFileName(); 186 | return path.Combine(temp, fileName); 187 | } 188 | 189 | private static ArgumentException CreateAlreadyExistsException(string argumentName, string path) 190 | { 191 | // Having the colliding path availabe as part of the exception is very useful for debugging. 192 | // However, paths can be considered sensitive information in some contexts (like web servers). 193 | // Thus, we add the path to the exception's data , rather than the message. 194 | var ex = new ArgumentException("File already exists", argumentName); 195 | ex.Data.Add("path", path); 196 | 197 | return ex; 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/LineEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace System.IO.Abstractions 6 | { 7 | internal sealed class LineEnumerable : IEnumerable 8 | { 9 | private readonly IFileInfo _file; 10 | private readonly Encoding _encoding; 11 | 12 | public LineEnumerable(IFileInfo file, Encoding encoding) 13 | { 14 | _file = file; 15 | _encoding = encoding; 16 | } 17 | 18 | public IEnumerator GetEnumerator() => new LineEnumerator(_file, _encoding); 19 | 20 | IEnumerator IEnumerable.GetEnumerator() => new LineEnumerator(_file, _encoding); 21 | } 22 | 23 | internal sealed class LineEnumerator : IEnumerator 24 | { 25 | private Stream _stream; 26 | private StreamReader _reader; 27 | private string _current; 28 | 29 | public LineEnumerator(IFileInfo file, Encoding encoding) 30 | { 31 | _stream = file.OpenRead(); 32 | _reader = encoding == null 33 | ? new StreamReader(_stream) 34 | : new StreamReader(_stream, encoding); 35 | } 36 | 37 | public string Current => _current; 38 | 39 | object IEnumerator.Current => _current; 40 | 41 | public void Dispose() 42 | { 43 | _reader?.Dispose(); 44 | _reader = null; 45 | _stream?.Dispose(); 46 | _stream = null; 47 | } 48 | 49 | public bool MoveNext() 50 | { 51 | _current = _reader.ReadLine(); 52 | return _current != null; 53 | } 54 | 55 | public void Reset() 56 | { 57 | throw new InvalidOperationException(); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace System.IO.Abstractions { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.IO.Abstractions.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The file '{0}' already exists.. 65 | /// 66 | internal static string CANNOT_OVERWRITE { 67 | get { 68 | return ResourceManager.GetString("CANNOT_OVERWRITE", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Could not find file '{0}'.. 74 | /// 75 | internal static string COULD_NOT_FIND_FILE_EXCEPTION { 76 | get { 77 | return ResourceManager.GetString("COULD_NOT_FIND_FILE_EXCEPTION", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Could not find a part of the path '{0}'.. 83 | /// 84 | internal static string COULD_NOT_FIND_PART_OF_PATH_EXCEPTION { 85 | get { 86 | return ResourceManager.GetString("COULD_NOT_FIND_PART_OF_PATH_EXCEPTION", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to '{0}' is not an ancestor of '{1}'.. 92 | /// 93 | internal static string NOT_AN_ANCESTOR { 94 | get { 95 | return ResourceManager.GetString("NOT_AN_ANCESTOR", resourceCulture); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The file '{0}' already exists. 122 | 123 | 124 | Could not find file '{0}'. 125 | 126 | 127 | Could not find a part of the path '{0}'. 128 | 129 | 130 | '{0}' is not an ancestor of '{1}'. 131 | 132 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/StringResources.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | 4 | namespace System.IO.Abstractions 5 | { 6 | internal static class StringResources 7 | { 8 | public static ResourceManager Manager { get; } = new ResourceManager( 9 | $"{typeof(StringResources).Namespace}.Resources", 10 | typeof(StringResources).GetTypeInfo().Assembly); 11 | 12 | public static string Format(string name, params object[] objects) 13 | { 14 | return String.Format(Manager.GetString(name), objects); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/System.IO.Abstractions.Extensions/System.IO.Abstractions.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | TestableIO.System.IO.Abstractions.Extensions 5 | net8.0;netstandard2.1;netstandard2.0;net472 6 | Convenience functionalities on top of System.IO.Abstractions 7 | System.IO.Abstractions 8 | $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../../ReleaseNotes.md")) 9 | 9.0 10 | README.md 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | True 25 | True 26 | Resources.resx 27 | 28 | 29 | 30 | 31 | 32 | ResXFileCodeGenerator 33 | Resources.Designer.cs 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 10 | true 11 | 12 | -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/DirectoryInfoExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | using Assert = NUnit.Framework.Legacy.ClassicAssert; 4 | 5 | namespace System.IO.Abstractions.Extensions.Tests 6 | { 7 | [TestFixture] 8 | public class DirectoryInfoExtensionsTests 9 | { 10 | [Test] 11 | public void SubDirectory_Extension_Test() 12 | { 13 | //arrange 14 | var fs = new FileSystem(); 15 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 16 | var guid = Guid.NewGuid().ToString(); 17 | var expectedPath = fs.Path.Combine(current.FullName, guid); 18 | 19 | //make sure directory doesn't exists 20 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 21 | 22 | //create directory 23 | var created = current.SubDirectory(guid); 24 | created.Create(); 25 | 26 | //assert it exists 27 | Assert.IsTrue(fs.Directory.Exists(expectedPath)); 28 | Assert.AreEqual(expectedPath, created.FullName); 29 | 30 | //delete directory 31 | created.Delete(); 32 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 33 | } 34 | 35 | [TestCase("test1", "test2")] 36 | [TestCase("test1", "", "test2")] 37 | [TestCase("test1", null, "test2")] 38 | public void SubDirectoryWithParams_Extension_Test(params string[] subFolders) 39 | { 40 | //arrange 41 | var fs = new FileSystem(); 42 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 43 | var expectedPath = fs.Path.Combine(current.FullName, "test1", "test2"); 44 | 45 | //make sure directory doesn't exists 46 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 47 | 48 | //create directory 49 | var created = current.SubDirectory(subFolders); 50 | created.Create(); 51 | 52 | //assert it exists 53 | Assert.IsTrue(fs.Directory.Exists(expectedPath)); 54 | Assert.AreEqual(expectedPath, created.FullName); 55 | 56 | //delete directory 57 | created.Delete(); 58 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 59 | } 60 | 61 | [TestCase("test1", "test2")] 62 | [TestCase("test1", "", "test2")] 63 | [TestCase("test1", null, "test2")] 64 | public void SubDirectoryWithIEnumerable_Extension_Test(params string[] names) 65 | { 66 | //arrange 67 | var fs = new FileSystem(); 68 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 69 | var expectedPath = fs.Path.Combine(current.FullName, "test1", "test2"); 70 | 71 | //make sure directory doesn't exists 72 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 73 | 74 | //create directory 75 | var list = new List(names); 76 | var created = current.SubDirectory(list); 77 | created.Create(); 78 | 79 | //assert it exists 80 | Assert.IsTrue(fs.Directory.Exists(expectedPath)); 81 | Assert.AreEqual(expectedPath, created.FullName); 82 | 83 | //delete directory 84 | created.Delete(); 85 | Assert.IsFalse(fs.Directory.Exists(expectedPath)); 86 | } 87 | 88 | [Test] 89 | public void File_Extension_Test() 90 | { 91 | //arrange 92 | var fs = new FileSystem(); 93 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 94 | var guid = Guid.NewGuid().ToString(); 95 | var expectedPath = fs.Path.Combine(current.FullName, guid); 96 | 97 | //make sure file doesn't exists 98 | Assert.IsFalse(fs.File.Exists(expectedPath)); 99 | 100 | //create file 101 | var created = current.File(guid); 102 | using (var stream = created.Create()) 103 | { 104 | stream.Dispose(); 105 | } 106 | 107 | //assert it exists 108 | Assert.IsTrue(fs.File.Exists(expectedPath)); 109 | Assert.AreEqual(expectedPath, created.FullName); 110 | 111 | //delete file 112 | created.Delete(); 113 | Assert.IsFalse(fs.File.Exists(expectedPath)); 114 | } 115 | 116 | [TestCase("test1", "test2", "test.txt")] 117 | [TestCase("test1", "", "test2", "test.txt")] 118 | [TestCase("test1", null, "test2", "test.txt")] 119 | 120 | public void FileWithParams_Extension_Test(params string[] names) 121 | { 122 | //arrange 123 | var fs = new FileSystem(); 124 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 125 | var expectedPath = fs.Path.Combine(current.FullName, "test1", "test2", "test.txt"); 126 | 127 | //make sure file doesn't exists 128 | Assert.IsFalse(fs.File.Exists(expectedPath)); 129 | 130 | //act, create file 131 | var created = current.File(names); 132 | created.Directory.Create(); 133 | using (var stream = created.Create()) 134 | { 135 | stream.Dispose(); 136 | } 137 | 138 | //assert it exists 139 | Assert.IsTrue(fs.File.Exists(expectedPath)); 140 | Assert.AreEqual(expectedPath, created.FullName); 141 | 142 | //delete file 143 | created.Delete(); 144 | created.Directory.Delete(); 145 | Assert.IsFalse(fs.File.Exists(expectedPath)); 146 | } 147 | 148 | [Test] 149 | public void ThrowIfNotFound_IfDirectoryDoesNotExists_ThrowsException() 150 | { 151 | //arrange 152 | var fs = new FileSystem(); 153 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 154 | var guid = Guid.NewGuid().ToString(); 155 | var directory = current.SubDirectory(guid); 156 | 157 | //act 158 | var exception = Assert.Throws(() => directory.ThrowIfNotFound()); 159 | 160 | //assert 161 | Assert.IsTrue(exception.Message.Contains(directory.FullName)); 162 | } 163 | 164 | [Test] 165 | public void ThrowIfNotFound_IfDirectoryExists_DoesNotThrowException() 166 | { 167 | //arrange 168 | var fs = new FileSystem(); 169 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 170 | var guid = Guid.NewGuid().ToString(); 171 | var directory = current.SubDirectory(guid); 172 | 173 | //act 174 | directory.Create(); 175 | directory.ThrowIfNotFound(); 176 | 177 | //assert 178 | Assert.IsTrue(directory.Exists); 179 | 180 | //cleanup 181 | directory.Delete(); 182 | } 183 | 184 | [Test] 185 | public void CopyTo_NonRecursiveWithSubfolder_DoesNotCopySubfolder() 186 | { 187 | //arrange 188 | var fs = new FileSystem(); 189 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 190 | 191 | //create directories 192 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 193 | var sourceSubDir = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir", "SubDir")); 194 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 195 | 196 | source.Create(); 197 | sourceSubDir.Create(); 198 | 199 | //make sure everything is set up as expected 200 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 201 | Assert.IsTrue(fs.Directory.Exists(sourceSubDir.FullName)); 202 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 203 | 204 | //act 205 | source.CopyTo(dest); 206 | 207 | var destSubDir = fs.DirectoryInfo.New(fs.Path.Combine(dest.FullName, "SubDir")); 208 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 209 | Assert.IsFalse(fs.Directory.Exists(destSubDir.FullName)); 210 | 211 | //cleanup 212 | workingDir.Delete(recursive: true); 213 | 214 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 215 | } 216 | 217 | [Test] 218 | public void CopyTo_NonRecursiveWithSubfolderWithFiles_DoesNotCopySubfolderAndSubfolderFiles() 219 | { 220 | //arrange 221 | var fs = new FileSystem(); 222 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 223 | 224 | //create directories 225 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 226 | var sourceSubDir = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir", "SubDir")); 227 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 228 | 229 | source.Create(); 230 | sourceSubDir.Create(); 231 | 232 | //create files 233 | var sourceFile = fs.FileInfo.New(fs.Path.Combine(source.FullName, "file.txt")); 234 | var sourceSubDirFile = fs.FileInfo.New(fs.Path.Combine(sourceSubDir.FullName, "file.txt")); 235 | 236 | sourceFile.Create().Dispose(); 237 | sourceSubDirFile.Create().Dispose(); 238 | 239 | //make sure everything is set up as expected 240 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 241 | Assert.IsTrue(fs.Directory.Exists(sourceSubDir.FullName)); 242 | Assert.IsTrue(fs.File.Exists(sourceFile.FullName)); 243 | Assert.IsTrue(fs.File.Exists(sourceSubDirFile.FullName)); 244 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 245 | 246 | //act 247 | source.CopyTo(dest); 248 | 249 | var destFile = fs.FileInfo.New(fs.Path.Combine(dest.FullName, "file.txt")); 250 | var destSubDir = fs.DirectoryInfo.New(fs.Path.Combine(dest.FullName, "SubDir")); 251 | var destSubDirFile = fs.FileInfo.New(fs.Path.Combine(destSubDir.FullName, "file.txt")); 252 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 253 | Assert.IsTrue(fs.File.Exists(destFile.FullName)); 254 | Assert.IsFalse(fs.Directory.Exists(destSubDir.FullName)); 255 | Assert.IsFalse(fs.File.Exists(destSubDirFile.FullName)); 256 | 257 | //cleanup 258 | workingDir.Delete(recursive: true); 259 | 260 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 261 | } 262 | 263 | [Test] 264 | public void CopyTo_RecursiveWithSubfolder_DoesCopySubfolder() 265 | { 266 | //arrange 267 | var fs = new FileSystem(); 268 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 269 | 270 | //create directories 271 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 272 | var sourceSubDir = fs.DirectoryInfo.New(fs.Path.Combine(source.FullName, "SubDir")); 273 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 274 | 275 | source.Create(); 276 | sourceSubDir.Create(); 277 | 278 | //make sure everything is set up as expected 279 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 280 | Assert.IsTrue(fs.Directory.Exists(sourceSubDir.FullName)); 281 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 282 | 283 | //act 284 | source.CopyTo(dest, recursive: true); 285 | 286 | var destSubDir = fs.DirectoryInfo.New(fs.Path.Combine(dest.FullName, "SubDir")); 287 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 288 | Assert.IsTrue(fs.Directory.Exists(destSubDir.FullName)); 289 | 290 | //cleanup 291 | workingDir.Delete(recursive: true); 292 | 293 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 294 | } 295 | 296 | [Test] 297 | public void CopyTo_RecursiveWithSubfolderWithFiles_DoesCopySubfolderAndSubfolderFiles() 298 | { 299 | //arrange 300 | var fs = new FileSystem(); 301 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 302 | 303 | //create directories 304 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 305 | var sourceSubDir = fs.DirectoryInfo.New(fs.Path.Combine(source.FullName, "SubDir")); 306 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 307 | 308 | source.Create(); 309 | sourceSubDir.Create(); 310 | 311 | //create files 312 | var sourceFile = fs.FileInfo.New(fs.Path.Combine(source.FullName, "file.txt")); 313 | var sourceSubDirFile = fs.FileInfo.New(fs.Path.Combine(sourceSubDir.FullName, "file.txt")); 314 | 315 | sourceFile.Create().Dispose(); 316 | sourceSubDirFile.Create().Dispose(); 317 | 318 | //make sure everything is set up as expected 319 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 320 | Assert.IsTrue(fs.Directory.Exists(sourceSubDir.FullName)); 321 | Assert.IsTrue(fs.File.Exists(sourceFile.FullName)); 322 | Assert.IsTrue(fs.File.Exists(sourceSubDirFile.FullName)); 323 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 324 | 325 | //act 326 | source.CopyTo(dest, recursive: true); 327 | 328 | var destFile = fs.FileInfo.New(fs.Path.Combine(dest.FullName, "file.txt")); 329 | var destSubDir = fs.DirectoryInfo.New(fs.Path.Combine(dest.FullName, "SubDir")); 330 | var destSubDirFile = fs.FileInfo.New(fs.Path.Combine(destSubDir.FullName, "file.txt")); 331 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 332 | Assert.IsTrue(fs.File.Exists(destFile.FullName)); 333 | Assert.IsTrue(fs.Directory.Exists(destSubDir.FullName)); 334 | Assert.IsTrue(fs.File.Exists(destSubDirFile.FullName)); 335 | 336 | //cleanup 337 | workingDir.Delete(recursive: true); 338 | 339 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 340 | } 341 | 342 | [Test] 343 | public void CopyTo_SourceDirDoesNotExists_ThrowsDirectoryNotFoundException() 344 | { 345 | //arrange 346 | var fs = new FileSystem(); 347 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 348 | 349 | //create directories 350 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 351 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 352 | 353 | //make sure everything is set up as expected 354 | Assert.IsFalse(fs.Directory.Exists(source.FullName)); 355 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 356 | 357 | //act 358 | Assert.That(() => source.CopyTo(dest), Throws.Exception.TypeOf().And.Message.Contains(source.FullName)); 359 | 360 | Assert.IsFalse(fs.File.Exists(source.FullName)); 361 | Assert.IsFalse(fs.File.Exists(dest.FullName)); 362 | } 363 | 364 | [Test] 365 | public void CopyTo_TargetDirAndParentDoesNotExist_CreatesTargetDirectoryHierarchy() 366 | { 367 | //arrange 368 | var fs = new FileSystem(); 369 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 370 | 371 | //create directories 372 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 373 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "ParentDir", "DestDir")); 374 | 375 | source.Create(); 376 | 377 | //make sure everything is set up as expected 378 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 379 | Assert.IsFalse(fs.Directory.Exists(dest.FullName)); 380 | 381 | //act 382 | source.CopyTo(dest); 383 | 384 | //assert 385 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 386 | 387 | //cleanup 388 | workingDir.Delete(recursive: true); 389 | 390 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 391 | } 392 | 393 | [Test] 394 | public void CopyTo_Overwrite_OverwritesWhenSet() 395 | { 396 | //arrange 397 | var fs = new FileSystem(); 398 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 399 | 400 | //create directories 401 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 402 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 403 | 404 | source.Create(); 405 | dest.Create(); 406 | 407 | //create files 408 | var sourceFile = fs.FileInfo.New(fs.Path.Combine(source.FullName, "file.txt")); 409 | var destFile = fs.FileInfo.New(fs.Path.Combine(dest.FullName, "file.txt")); 410 | 411 | var sourceFileContent = new[] { nameof(sourceFile) }; 412 | sourceFile.WriteLines(sourceFileContent); 413 | var destFileContent = new[] { nameof(destFile) }; 414 | destFile.WriteLines(destFileContent); 415 | 416 | //make sure everything is set up as expected 417 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 418 | Assert.IsTrue(fs.File.Exists(sourceFile.FullName)); 419 | Assert.AreEqual(fs.File.ReadAllLines(sourceFile.FullName), sourceFileContent); 420 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 421 | Assert.IsTrue(fs.File.Exists(destFile.FullName)); 422 | Assert.AreEqual(fs.File.ReadAllLines(destFile.FullName), destFileContent); 423 | 424 | //act 425 | source.CopyTo(dest, overwrite: true); 426 | 427 | //assert 428 | Assert.AreEqual(fs.File.ReadAllLines(destFile.FullName), sourceFileContent); 429 | 430 | //cleanup 431 | workingDir.Delete(recursive: true); 432 | 433 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 434 | } 435 | 436 | [Test] 437 | public void CopyTo_Overwrite_DoesNotOverwritesWhenNotSet() 438 | { 439 | //arrange 440 | var fs = new FileSystem(); 441 | var workingDir = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()).CreateSubdirectory(Guid.NewGuid().ToString()); 442 | 443 | //create directories 444 | var source = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "SourceDir")); 445 | var dest = fs.DirectoryInfo.New(fs.Path.Combine(workingDir.FullName, "DestDir")); 446 | 447 | source.Create(); 448 | dest.Create(); 449 | 450 | //create files 451 | var sourceFile = fs.FileInfo.New(fs.Path.Combine(source.FullName, "file.txt")); 452 | var destFile = fs.FileInfo.New(fs.Path.Combine(dest.FullName, "file.txt")); 453 | 454 | var sourceFileContent = new[] { nameof(sourceFile) }; 455 | sourceFile.WriteLines(sourceFileContent); 456 | var destFileContent = new[] { nameof(destFile) }; 457 | destFile.WriteLines(destFileContent); 458 | 459 | //make sure everything is set up as expected 460 | Assert.IsTrue(fs.Directory.Exists(source.FullName)); 461 | Assert.IsTrue(fs.File.Exists(sourceFile.FullName)); 462 | Assert.AreEqual(fs.File.ReadAllLines(sourceFile.FullName), sourceFileContent); 463 | Assert.IsTrue(fs.Directory.Exists(dest.FullName)); 464 | Assert.IsTrue(fs.File.Exists(destFile.FullName)); 465 | Assert.AreEqual(fs.File.ReadAllLines(destFile.FullName), destFileContent); 466 | 467 | //act 468 | Assert.That(() => source.CopyTo(dest, overwrite: false), Throws.Exception.TypeOf().And.Message.Contains(destFile.FullName)); 469 | 470 | //assert 471 | Assert.AreEqual(fs.File.ReadAllLines(destFile.FullName), destFileContent); 472 | 473 | //cleanup 474 | workingDir.Delete(recursive: true); 475 | 476 | Assert.IsFalse(fs.File.Exists(workingDir.FullName)); 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/DisposableDirectoryTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Assert = NUnit.Framework.Legacy.ClassicAssert; 3 | 4 | namespace System.IO.Abstractions.Extensions.Tests 5 | { 6 | [TestFixture] 7 | public class DisposableDirectoryTests 8 | { 9 | [Test] 10 | public void DisposableDirectory_Throws_ArgumentNullException_For_Null_IDirectoryInfo_Test() 11 | { 12 | Assert.Throws(() => new DisposableDirectory(null)); 13 | } 14 | 15 | [Test] 16 | public void DisposableDirectory_DeleteRecursive_On_Dispose_Test() 17 | { 18 | // Arrange 19 | var fs = new FileSystem(); 20 | var path = fs.Path.Combine(fs.Directory.GetCurrentDirectory(), fs.Path.GetRandomFileName()); 21 | var dirInfo = fs.DirectoryInfo.New(path); 22 | 23 | // Create a subdirectory to ensure recursive delete 24 | dirInfo.CreateSubdirectory(Guid.NewGuid().ToString()); 25 | 26 | // Assert directory exists 27 | Assert.IsTrue(fs.Directory.Exists(path), "Directory should exist"); 28 | Assert.IsTrue(dirInfo.Exists, "IDirectoryInfo.Exists should be true"); 29 | 30 | // Act 31 | var disposableDirectory = new DisposableDirectory(dirInfo); 32 | disposableDirectory.Dispose(); 33 | 34 | // Assert directory is deleted 35 | Assert.IsFalse(fs.Directory.Exists(path), "Directory should not exist"); 36 | Assert.IsFalse(dirInfo.Exists, "IDirectoryInfo.Exists should be false"); 37 | 38 | // Assert a second dispose does not throw 39 | Assert.DoesNotThrow(() => disposableDirectory.Dispose()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/DisposableFileTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Assert = NUnit.Framework.Legacy.ClassicAssert; 3 | 4 | namespace System.IO.Abstractions.Extensions.Tests 5 | { 6 | [TestFixture] 7 | public class DisposableFileTests 8 | { 9 | [Test] 10 | public void DisposableFile_Throws_ArgumentNullException_For_Null_IFileInfo_Test() 11 | { 12 | Assert.Throws(() => new DisposableFile(null)); 13 | } 14 | 15 | [Test] 16 | public void DisposableFile_Delete_On_Dispose_Test() 17 | { 18 | // Arrange 19 | var fs = new FileSystem(); 20 | var path = fs.Path.Combine(fs.Directory.GetCurrentDirectory(), fs.Path.GetRandomFileName()); 21 | var fileInfo = fs.FileInfo.New(path); 22 | fileInfo.Create().Dispose(); 23 | 24 | // Assert file exists 25 | Assert.IsTrue(fs.File.Exists(path), "File exists"); 26 | Assert.IsTrue(fileInfo.Exists, "IFileInfo.Exists should be true"); 27 | 28 | // Act 29 | var disposableFile = new DisposableFile(fileInfo); 30 | disposableFile.Dispose(); 31 | 32 | // Assert directory is deleted 33 | Assert.IsFalse(fs.File.Exists(path), "File does not exist"); 34 | Assert.IsFalse(fileInfo.Exists, "IFileInfo.Exists should be false"); 35 | 36 | // Assert a second dispose does not throw 37 | Assert.DoesNotThrow(() => disposableFile.Dispose()); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/FileInfoExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Linq; 3 | using System.Text; 4 | using Assert = NUnit.Framework.Legacy.ClassicAssert; 5 | 6 | namespace System.IO.Abstractions.Extensions.Tests 7 | { 8 | [TestFixture] 9 | public class FileInfoExtensionsTests 10 | { 11 | [Test] 12 | public void ThrowIfNotFound_IfFileDoesNotExists_ThrowsException() 13 | { 14 | //arrange 15 | var fs = new FileSystem(); 16 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 17 | var guid = Guid.NewGuid().ToString(); 18 | var file = current.File(guid); 19 | 20 | //act 21 | var exception = Assert.Throws(() => file.ThrowIfNotFound()); 22 | 23 | //assert 24 | Assert.IsTrue(exception.Message.Contains(file.FullName)); 25 | } 26 | 27 | [Test] 28 | public void ThrowIfNotFound_IfFileExists_DoesNotThrowException() 29 | { 30 | //arrange 31 | var fs = new FileSystem(); 32 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 33 | var guid = Guid.NewGuid().ToString(); 34 | var file = current.File(guid); 35 | 36 | //act 37 | file.Truncate(); 38 | file.ThrowIfNotFound(); 39 | 40 | //assert 41 | Assert.IsTrue(file.Exists); 42 | 43 | //cleanup 44 | file.Delete(); 45 | } 46 | 47 | [Test] 48 | public void Truncate_AnExistingFileWithContent_FileExistsAndIsEmpty() 49 | { 50 | //arrange 51 | var fs = new FileSystem(); 52 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 53 | var guid = Guid.NewGuid().ToString(); 54 | var file = current.File(guid); 55 | //create file 56 | using (var stream = file.OpenWrite()) 57 | using (var writer = new StreamWriter(stream, Encoding.UTF8)) 58 | { 59 | writer.WriteLine("test"); 60 | } 61 | file.Refresh(); 62 | Assert.IsTrue(file.Exists); 63 | Assert.IsTrue(file.Length >= 4); 64 | 65 | //act 66 | file.Truncate(); 67 | 68 | //assert 69 | file.Refresh(); 70 | Assert.AreEqual(0, file.Length); 71 | } 72 | 73 | [Test] 74 | public void Truncate_ANewFile_FileExistsAndIsEmpty() 75 | { 76 | //arrange 77 | var fs = new FileSystem(); 78 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 79 | var guid = Guid.NewGuid().ToString(); 80 | var file = current.File(guid); 81 | Assert.IsFalse(file.Exists); 82 | 83 | //act 84 | file.Truncate(); 85 | 86 | //assert 87 | file.Refresh(); 88 | Assert.AreEqual(0, file.Length); 89 | } 90 | 91 | [TestCase("line1", "line2", "line3")] 92 | [TestCase("line1", "", "line3")] 93 | public void EnumerateLines_ReadFromExistingFile_ReturnsLines(params string[] content) 94 | { 95 | //arrange 96 | var fs = new FileSystem(); 97 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 98 | var guid = Guid.NewGuid().ToString(); 99 | var file = current.File(guid); 100 | //create file 101 | using (var stream = file.OpenWrite()) 102 | using (var writer = new StreamWriter(stream, Encoding.UTF8)) 103 | { 104 | foreach (var line in content) 105 | writer.WriteLine(line); 106 | } 107 | 108 | //act 109 | var actual = file.EnumerateLines().ToArray(); 110 | 111 | //assert 112 | Assert.AreEqual(content.Length, actual.Length); 113 | for (int i = 0; i < content.Length; i++) 114 | { 115 | Assert.AreEqual(content[i], actual[i]); 116 | } 117 | } 118 | 119 | [TestCase("line1", "line2", "line3")] 120 | [TestCase("line1", "", "line3")] 121 | public void WriteLines_WriteLinesToNewFile_LinesAreWritten(params string[] content) 122 | { 123 | //arrange 124 | var fs = new FileSystem(); 125 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 126 | var guid = Guid.NewGuid().ToString(); 127 | var file = current.File(guid); 128 | 129 | //act 130 | Assert.IsFalse(file.Exists); 131 | file.WriteLines(content); 132 | var actual = file.EnumerateLines().ToArray(); 133 | 134 | //assert 135 | Assert.AreEqual(content.Length, actual.Length); 136 | for (int i = 0; i < content.Length; i++) 137 | { 138 | Assert.AreEqual(content[i], actual[i]); 139 | } 140 | } 141 | 142 | [TestCase("line1", "line2", "line3")] 143 | [TestCase("line1", "", "line3")] 144 | public void WriteLines_WriteLinesToExistingFileWithOverwriteDisabled_ThrowsIOException(params string[] content) 145 | { 146 | //arrange 147 | var fs = new FileSystem(); 148 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 149 | var guid = Guid.NewGuid().ToString(); 150 | var file = current.File(guid); 151 | file.Truncate(); 152 | 153 | //act & assert 154 | Assert.IsTrue(file.Exists); 155 | //call WriteLines with both overwrite parameter ommitted or set to false 156 | var ex1 = Assert.Throws(() => file.WriteLines(content)); 157 | var ex2 = Assert.Throws(() => file.WriteLines(content, false)); 158 | 159 | Assert.IsTrue(ex1.Message.Contains(file.FullName)); 160 | Assert.IsTrue(ex2.Message.Contains(file.FullName)); 161 | } 162 | 163 | [TestCase("line1", "line2", "line3")] 164 | [TestCase("line1", "", "line3")] 165 | public void WriteLines_WriteLinesToExistingFileWithOverwriteEnabled_FileIsTruncatedAndLinesAreWritten(params string[] content) 166 | { 167 | //arrange 168 | var fs = new FileSystem(); 169 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 170 | var guid = Guid.NewGuid().ToString(); 171 | var file = current.File(guid); 172 | 173 | //create file with long content 174 | var data = Encoding.UTF8.GetBytes("line5 line4 line3 line2 line1"); 175 | using (var stream = file.OpenWrite()) 176 | { 177 | stream.Write(data, 0, data.Length); 178 | stream.Dispose(); 179 | } 180 | 181 | //act 182 | Assert.IsTrue(file.Exists); 183 | file.WriteLines(content, overwrite: true); 184 | var actual = file.EnumerateLines().ToArray(); 185 | 186 | //assert 187 | Assert.AreEqual(content.Length, actual.Length); 188 | for (int i = 0; i < content.Length; i++) 189 | { 190 | Assert.AreEqual(content[i], actual[i]); 191 | } 192 | } 193 | 194 | [TestCase("line1", "line2", "line3")] 195 | [TestCase("line1", "", "line3")] 196 | public void WriteLinesWithUTF16Encoding_WriteLinesToNewFile_LinesAreWritten(params string[] content) 197 | { 198 | //arrange 199 | var fs = new FileSystem(); 200 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 201 | var guid = Guid.NewGuid().ToString(); 202 | var file = current.File(guid); 203 | 204 | //act 205 | Assert.IsFalse(file.Exists); 206 | file.WriteLines(content, Encoding.Unicode); 207 | var actual = file.EnumerateLines(Encoding.Unicode).ToArray(); 208 | 209 | //assert 210 | Assert.AreEqual(content.Length, actual.Length); 211 | for (int i = 0; i < content.Length; i++) 212 | { 213 | Assert.AreEqual(content[i], actual[i]); 214 | } 215 | } 216 | 217 | [TestCase("line1", "line2", "line3")] 218 | [TestCase("line1", "", "line3")] 219 | public void WriteLinesWithUTF16Encoding_WriteLinesToExistingFileWithOverwriteDisabled_ThrowsIOException(params string[] content) 220 | { 221 | //arrange 222 | var fs = new FileSystem(); 223 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 224 | var guid = Guid.NewGuid().ToString(); 225 | var file = current.File(guid); 226 | file.Truncate(); 227 | 228 | //act & assert 229 | Assert.IsTrue(file.Exists); 230 | //call WriteLines with both overwrite parameter ommitted or set to false 231 | var ex1 = Assert.Throws(() => file.WriteLines(content, Encoding.Unicode)); 232 | var ex2 = Assert.Throws(() => file.WriteLines(content, Encoding.Unicode, false)); 233 | 234 | Assert.IsTrue(ex1.Message.Contains(file.FullName)); 235 | Assert.IsTrue(ex2.Message.Contains(file.FullName)); 236 | } 237 | 238 | [TestCase("line1", "line2", "line3")] 239 | [TestCase("line1", "", "line3")] 240 | public void WriteLinesWithUTF16Encoding_WriteLinesToExistingFileWithOverwriteEnabled_FileIsTruncatedAndLinesAreWritten(params string[] content) 241 | { 242 | //arrange 243 | var fs = new FileSystem(); 244 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 245 | var guid = Guid.NewGuid().ToString(); 246 | var file = current.File(guid); 247 | 248 | //create file with long content 249 | var data = Encoding.Unicode.GetBytes("line5 line4 line3 line2 line1"); 250 | using (var stream = file.OpenWrite()) 251 | { 252 | stream.Write(data, 0, data.Length); 253 | stream.Dispose(); 254 | } 255 | 256 | //act 257 | Assert.IsTrue(file.Exists); 258 | file.WriteLines(content, Encoding.Unicode, overwrite: true); 259 | var actual = file.EnumerateLines(Encoding.Unicode).ToArray(); 260 | 261 | //assert 262 | Assert.AreEqual(content.Length, actual.Length); 263 | for (int i = 0; i < content.Length; i++) 264 | { 265 | Assert.AreEqual(content[i], actual[i]); 266 | } 267 | } 268 | 269 | [TestCase("line1", "line2", "line3")] 270 | [TestCase("line1", "", "line3")] 271 | public void AppendText_FileExistsAndHasText_LinesAreAppended(params string[] append) 272 | { 273 | //arrange 274 | var initial = new[] { "test1", "test2", "test3" }; 275 | var fs = new FileSystem(); 276 | var current = fs.DirectoryInfo.New(fs.Directory.GetCurrentDirectory()); 277 | var guid = Guid.NewGuid().ToString(); 278 | var file = current.File(guid); 279 | file.WriteLines(initial); 280 | 281 | //act 282 | file.AppendLines(append); 283 | 284 | //assert 285 | var expected = initial.Concat(append).ToArray(); 286 | var actual = file.EnumerateLines().ToArray(); 287 | 288 | Assert.AreEqual(expected.Length, actual.Length); 289 | for (int i = 0; i < expected.Length; i++) 290 | { 291 | Assert.AreEqual(expected[i], actual[i]); 292 | } 293 | } 294 | } 295 | } -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/FileSystemExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Assert = NUnit.Framework.Legacy.ClassicAssert; 3 | 4 | namespace System.IO.Abstractions.Extensions.Tests 5 | { 6 | [TestFixture] 7 | public class FileSystemExtensionsTests 8 | { 9 | private class CustomDisposableDirectory : DisposableDirectory 10 | { 11 | public bool DeleteFileSystemInfoWasCalled { get; private set; } 12 | 13 | public CustomDisposableDirectory(IDirectoryInfo directoryInfo) : base(directoryInfo) 14 | { 15 | } 16 | 17 | protected override void DeleteFileSystemInfo() 18 | { 19 | DeleteFileSystemInfoWasCalled = true; 20 | base.DeleteFileSystemInfo(); 21 | } 22 | } 23 | 24 | private class CustomDisposableFile : DisposableFile 25 | { 26 | public bool DeleteFileSystemInfoWasCalled { get; private set; } 27 | 28 | public CustomDisposableFile(IFileInfo fileInfo) : base(fileInfo) 29 | { 30 | } 31 | 32 | protected override void DeleteFileSystemInfo() 33 | { 34 | DeleteFileSystemInfoWasCalled = true; 35 | base.DeleteFileSystemInfo(); 36 | } 37 | } 38 | 39 | [Test] 40 | public void CurrentDirectoryTest() 41 | { 42 | var fs = new FileSystem(); 43 | var fullName = fs.CurrentDirectory().FullName; 44 | 45 | Assert.IsFalse(String.IsNullOrWhiteSpace(fullName)); 46 | NUnit.Framework.Assert.That(fullName, Is.EqualTo(Environment.CurrentDirectory)); 47 | } 48 | 49 | [Test] 50 | public void CreateDisposableDirectory_Temp_Path_Test() 51 | { 52 | // Arrange 53 | var fs = new FileSystem(); 54 | string path; 55 | 56 | // Act 57 | using (_ = fs.CreateDisposableDirectory(out var dir)) 58 | { 59 | path = dir.FullName; 60 | 61 | Assert.IsTrue(dir.Exists, "Directory should exist"); 62 | Assert.IsTrue( 63 | path.StartsWith(fs.Path.GetTempPath(), StringComparison.Ordinal), 64 | "Directory should be in temp path"); 65 | } 66 | 67 | // Assert directory is deleted 68 | Assert.IsFalse(fs.Directory.Exists(path), "Directory should not exist"); 69 | } 70 | 71 | [Test] 72 | public void CreateDisposableDirectory_Already_Exists_Test() 73 | { 74 | // Arrange 75 | var fs = new FileSystem(); 76 | var path = fs.Path.Combine(fs.Path.GetTempPath(), fs.Path.GetRandomFileName()); 77 | fs.Directory.CreateDirectory(path); 78 | 79 | // Assert 80 | var ex = Assert.Throws(() => fs.CreateDisposableDirectory(path, out _)); 81 | Assert.True(ex.Data["path"].ToString() == path, "Exception data should contain colliding path to aid with debugging"); 82 | 83 | // Delete colliding directory 84 | fs.Directory.Delete(path); 85 | Assert.IsFalse(fs.Directory.Exists(path), "Directory should not exist"); 86 | } 87 | 88 | [Test] 89 | public void CreateDisposableDirectory_Custom_IDisposable_Test() 90 | { 91 | // Arrange 92 | var fs = new FileSystem(); 93 | string path = null; 94 | 95 | // Act 96 | CustomDisposableDirectory customDisposable; 97 | using (customDisposable = fs.CreateDisposableDirectory(dir => new CustomDisposableDirectory(dir), out var dirInfo)) 98 | { 99 | path = dirInfo.FullName; 100 | 101 | Assert.IsTrue(dirInfo.Exists, "Directory should exist"); 102 | Assert.IsFalse(customDisposable.DeleteFileSystemInfoWasCalled, "Delete should not have been called yet"); 103 | } 104 | 105 | // Assert directory is deleted 106 | Assert.IsNotNull(path); 107 | Assert.IsFalse(fs.Directory.Exists(path), "Directory should not exist"); 108 | Assert.IsTrue(customDisposable.DeleteFileSystemInfoWasCalled, "Custom disposable delete should have been called"); 109 | } 110 | 111 | [Test] 112 | public void CreateDisposableFile_Temp_Path_Test() 113 | { 114 | // Arrange 115 | var fs = new FileSystem(); 116 | string path; 117 | 118 | // Act 119 | using (_ = fs.CreateDisposableFile(out var file)) 120 | { 121 | path = file.FullName; 122 | 123 | Assert.That(file.Exists, Is.True, "File should exist"); 124 | Assert.IsTrue( 125 | path.StartsWith(fs.Path.GetTempPath(), StringComparison.Ordinal), 126 | "File should be in temp path"); 127 | } 128 | 129 | // Assert file is deleted 130 | Assert.That(fs.File.Exists(path), Is.False, "File should not exist"); 131 | } 132 | 133 | [Test] 134 | public void CreateDisposableFile_Already_Exists_Test() 135 | { 136 | // Arrange 137 | var fs = new FileSystem(); 138 | var path = fs.Path.Combine(fs.Path.GetTempPath(), fs.Path.GetRandomFileName()); 139 | fs.File.Create(path).Dispose(); 140 | 141 | // Assert 142 | var ex = Assert.Throws(() => fs.CreateDisposableFile(path, out _)); 143 | Assert.True(ex.Data["path"].ToString() == path, "Exception data should contain colliding path to aid with debugging"); 144 | 145 | // Delete colliding file 146 | fs.File.Delete(path); 147 | Assert.IsFalse(fs.File.Exists(path), "File should not exist"); 148 | } 149 | 150 | [Test] 151 | public void CreateDisposableFile_Custom_IDisposable_Test() 152 | { 153 | // Arrange 154 | var fs = new FileSystem(); 155 | string path = null; 156 | 157 | // Act 158 | CustomDisposableFile customDisposable; 159 | using (customDisposable = fs.CreateDisposableFile(dir => new CustomDisposableFile(dir), out var fileInfo)) 160 | { 161 | path = fileInfo.FullName; 162 | 163 | Assert.IsTrue(fileInfo.Exists, "File should exist"); 164 | Assert.IsFalse(customDisposable.DeleteFileSystemInfoWasCalled, "Delete should not have been called yet"); 165 | } 166 | 167 | // Assert file is deleted 168 | Assert.IsNotNull(path); 169 | Assert.IsFalse(fs.File.Exists(path), "File should not exist"); 170 | Assert.IsTrue(customDisposable.DeleteFileSystemInfoWasCalled, "Custom disposable delete should have been called"); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/System.IO.Abstractions.Extensions.Tests/System.IO.Abstractions.Extensions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net9.0;net8.0 5 | $(TargetFrameworks);net472 6 | false 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "22.0", 4 | "assemblyVersion": { 5 | "precision": "major" 6 | }, 7 | "publicReleaseRefSpec": [ 8 | "^refs/heads/main$" 9 | ], 10 | "cloudBuild": { 11 | "buildNumber": { 12 | "enabled": true 13 | } 14 | } 15 | } --------------------------------------------------------------------------------