├── .gitignore ├── .travis.yml ├── Cactus.Fileserver.Aspnet ├── Cactus.Fileserver.Aspnet.csproj ├── Config │ └── AppBuilderExtension.cs ├── Dto │ └── ResponseDto.cs ├── HttpExtensions.cs └── Middleware │ ├── AddFileHandler.cs │ ├── AddMultipartContentHandler.cs │ ├── DeleteFileHandler.cs │ ├── GetFileHandler.cs │ └── RedirectToInternalStorageHandler.cs ├── Cactus.Fileserver.AzureStorage ├── AzureFileStorage.cs └── Cactus.Fileserver.AzureStorage.csproj ├── Cactus.Fileserver.ImageResizer ├── Assembly.cs ├── Cactus.Fileserver.ImageResizer.csproj ├── ConfigurationExtensions.cs ├── DynamicResizeMiddleware.cs ├── HttpRequestExtension.cs ├── ImageResizerService.cs └── Utils │ └── ResizeInstructions.cs ├── Cactus.Fileserver.LocalStorage.Test ├── AllInTheSameFolderUriResolverTests.cs ├── Cactus.Fileserver.LocalStorage.Test.csproj ├── FolderUriResolverTest.cs ├── LocalFileStorageTests.cs └── LocalMetaInfoStorageTests.cs ├── Cactus.Fileserver.LocalStorage ├── Assembly.cs ├── Cactus.Fileserver.LocalStorage.csproj ├── Config │ ├── ConfigurationExtensions.cs │ ├── LocalMetaStorageOptions.cs │ └── LocalStorageOptions.cs ├── LocalFileStorage.cs ├── LocalMetaInfoStorage.cs ├── SingleFolderUriResolver.cs ├── SubfolderUriResolver.cs └── UriResolver.cs ├── Cactus.Fileserver.S3Storage ├── Cactus.Fileserver.S3Storage.csproj ├── ConfigurationExtensions.cs ├── S3FileStorage.cs ├── S3FileStorageOptions.cs └── UriResolver.cs ├── Cactus.Fileserver.Tests ├── Cactus.Fileserver.Tests.csproj ├── Integration │ ├── CreateReadDeleteTest.cs │ ├── FileserverTestHost.cs │ └── ImageResizeTest.cs ├── Unit │ ├── ImageResizerTest.cs │ ├── RandomNameProviderTest.cs │ ├── ResizeInstructionTest.cs │ └── UriExtensionTest.cs ├── kartman.png └── log4net.config ├── Cactus.Fileserver.sln ├── Cactus.Fileserver ├── Assembly.cs ├── Cactus.Fileserver.csproj ├── FileStorageService.cs ├── IFileStorageService.cs ├── Model │ ├── IMetaInfo.cs │ └── MetaInfo.cs ├── Storage │ ├── IFileStorage.cs │ ├── IMetaInfoStorage.cs │ ├── IStoredNameProvider.cs │ └── RandomNameProvider.cs └── UriExtension.cs ├── Examples └── Cactus.Fileserver.Simple │ ├── Cactus.Fileserver.Simple.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── log4net.config │ └── wwwroot │ ├── .json │ ├── 17GfJps2PCX06jSd.txt │ ├── 3tenEHtSwZFfqXpx.txt │ ├── 4sRpySbooGfEQXFW.jpg │ ├── 4sRpySbooGfEQXFW.jpg.json │ ├── 59kc8cD7lyITeniC.jpg │ ├── 59kc8cD7lyITeniC.jpg.json │ ├── 623U8TvsnTwOKYMU.jpg │ ├── 623U8TvsnTwOKYMU.jpg.json │ ├── NonEGsJqfdv7PZCd.jpg │ ├── NonEGsJqfdv7PZCd.jpg.json │ ├── RRmhkED4tndt0X1B.jpg │ ├── RRmhkED4tndt0X1B.jpg.json │ ├── Wqf0k8WL10Ht3JAc.jpg │ ├── Wqf0k8WL10Ht3JAc.jpg.json │ ├── X8Q6qShBQglPl_w8.jpg │ ├── X8Q6qShBQglPl_w8.jpg.json │ ├── _CgVvhMoija-Q-IV.txt │ ├── bYqzr8vZ7V8xm53I.jpg │ ├── bYqzr8vZ7V8xm53I.jpg.json │ ├── dN7ucueKCfKmDb9D.jpg │ ├── dN7ucueKCfKmDb9D.jpg.json │ ├── eSyNpOSpJV55F8-v.txt │ ├── fXINhOS0ku4DuR9U │ ├── fXINhOS0ku4DuR9U.json │ ├── i78fV6TsnMbaN6r8.jpg │ ├── i78fV6TsnMbaN6r8.jpg.json │ ├── juRt0OK0tWprDnBH.jpg │ ├── juRt0OK0tWprDnBH.jpg.json │ ├── njKrLZONPbU871tS.jpg │ ├── njKrLZONPbU871tS.jpg.json │ ├── nothing.txt │ ├── zGQbOo2yH6L98skc.jpg │ └── zGQbOo2yH6L98skc.jpg.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Download this file using PowerShell v3 under Windows with the following comand 2 | # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # Build results 11 | [Dd]ebug/ 12 | [Dd]ebugPublic/ 13 | [Rr]elease/ 14 | [Rr]eleases/ 15 | x64/ 16 | x86/ 17 | build/ 18 | bld/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # NuGet Packages 25 | *.nupkg 26 | # The packages folder can be ignored because of Package Restore 27 | **/packages/* 28 | **/node_modules/* 29 | # except build/, which is used as an MSBuild target. 30 | !**/packages/build/ 31 | # Uncomment if necessary however generally it will be regenerated when needed 32 | #!**/packages/repositories.config 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | *_i.c 39 | *_p.c 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.log 60 | *.scc 61 | 62 | # OS generated files # 63 | .DS_Store* 64 | Icon? 65 | 66 | # Visual C++ cache files 67 | ipch/ 68 | *.aps 69 | *.ncb 70 | *.opensdf 71 | *.sdf 72 | *.cachefile 73 | 74 | # Visual Studio profiler 75 | *.psess 76 | *.vsp 77 | *.vspx 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | 86 | # TeamCity is a build add-in 87 | _TeamCity* 88 | 89 | # DotCover is a Code Coverage Tool 90 | *.dotCover 91 | 92 | # NCrunch 93 | *.ncrunch* 94 | .*crunch*.local.xml 95 | 96 | # Installshield output folder 97 | [Ee]xpress/ 98 | 99 | # DocProject is a documentation generator add-in 100 | DocProject/buildhelp/ 101 | DocProject/Help/*.HxT 102 | DocProject/Help/*.HxC 103 | DocProject/Help/*.hhc 104 | DocProject/Help/*.hhk 105 | DocProject/Help/*.hhp 106 | DocProject/Help/Html2 107 | DocProject/Help/html 108 | 109 | # Click-Once directory 110 | publish/ 111 | 112 | # Publish Web Output 113 | *.Publish.xml 114 | 115 | # Windows Azure Build Output 116 | csx 117 | *.build.csdef 118 | 119 | # Windows Store app package directory 120 | AppPackages/ 121 | 122 | # Others 123 | *.Cache 124 | ClientBin/ 125 | 126 | ~$* 127 | *~ 128 | *.dbmdl 129 | *.[Pp]ublish.xml 130 | *.pfx 131 | *.publishsettings 132 | modulesbin/ 133 | tempbin/ 134 | 135 | # EPiServer Site file (VPP) 136 | AppData/ 137 | 138 | # RIA/Silverlight projects 139 | Generated_Code/ 140 | 141 | # Backup & report files from converting an old project file to a newer 142 | # Visual Studio version. Backup files are not needed, because we have git ;-) 143 | _UpgradeReport_Files/ 144 | Backup*/ 145 | UpgradeLog*.XML 146 | UpgradeLog*.htm 147 | 148 | # vim 149 | *.txt~ 150 | *.swp 151 | *.swo 152 | 153 | # svn 154 | .svn 155 | 156 | # Remainings from resolvings conflicts in Source Control 157 | *.orig 158 | 159 | # SQL Server files 160 | **/App_Data/*.mdf 161 | **/App_Data/*.ldf 162 | **/App_Data/*.sdf 163 | 164 | 165 | #LightSwitch generated files 166 | GeneratedArtifacts/ 167 | _Pvt_Extensions/ 168 | ModelManifest.xml 169 | 170 | # ========================= 171 | # Windows detritus 172 | # ========================= 173 | 174 | # Windows image file caches 175 | Thumbs.db 176 | ehthumbs.db 177 | 178 | # Folder config file 179 | Desktop.ini 180 | 181 | # Recycle Bin used on file shares 182 | $RECYCLE.BIN/ 183 | 184 | # Mac desktop service store files 185 | .DS_Store 186 | 187 | # SASS Compiler cache 188 | .sass-cache 189 | 190 | # Visual Studio 2014 CTP 191 | **/*.sln.ide 192 | 193 | # Visual Studio temp something 194 | .vs/ 195 | 196 | ##### 197 | # End of core ignore list, below put you custom 'per project' settings (patterns or path) 198 | ##### 199 | Sommelier.Api/Sommelier.Api/UploadFiles/ 200 | Cactus.Fileserver.ImageResizer.Nuget/ 201 | Cactus.Fileserver.Owin.Nuget/ 202 | Examples/Cactus.Fileserver.Simple/wwwroot/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 3.1 4 | solution: Cactus.Fileserver.sln 5 | env: 6 | global: 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | script: 9 | - dotnet restore 10 | - dotnet build 11 | - dotnet test Cactus.Fileserver.Tests/Cactus.Fileserver.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 12 | - dotnet test Cactus.Fileserver.LocalStorage.Test/Cactus.Fileserver.LocalStorage.Test.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 13 | after_script: 14 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Cactus.Fileserver.Aspnet.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 4.0.4 6 | MIT 7 | https://github.com/CactusSoft/Cactus.Fileserver 8 | https://github.com/CactusSoft/Cactus.Fileserver 9 | GitHub 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Config/AppBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Cactus.Fileserver.Aspnet.Middleware; 4 | using Microsoft.AspNetCore.Builder; 5 | 6 | namespace Cactus.Fileserver.Aspnet.Config 7 | { 8 | public static class AppBuilderExtension 9 | { 10 | public static IApplicationBuilder UseDelFile(this IApplicationBuilder app) 11 | { 12 | app.MapWhen(c => HttpMethod.Delete.Method.Equals(c.Request.Method, StringComparison.OrdinalIgnoreCase) && 13 | c.Request.Path.HasValue, 14 | builder => builder.UseMiddleware()); 15 | return app; 16 | } 17 | 18 | public static IApplicationBuilder UseDelFile(this IApplicationBuilder app) 19 | { 20 | app.MapWhen(c => HttpMethod.Delete.Method.Equals(c.Request.Method, StringComparison.OrdinalIgnoreCase) && 21 | c.Request.Path.HasValue, 22 | builder => builder.UseMiddleware()); 23 | return app; 24 | } 25 | 26 | public static IApplicationBuilder UseAddFile(this IApplicationBuilder app) 27 | { 28 | app.MapWhen(c => HttpMethod.Post.Method.Equals(c.Request.Method, StringComparison.OrdinalIgnoreCase), 29 | builder => builder 30 | .UseMiddleware() 31 | .UseMiddleware()); 32 | return app; 33 | } 34 | 35 | public static IApplicationBuilder UseAddFile(this IApplicationBuilder app) 36 | { 37 | app.MapWhen(c => HttpMethod.Post.Method.Equals(c.Request.Method, StringComparison.OrdinalIgnoreCase), 38 | builder => builder.UseMiddleware()); 39 | return app; 40 | } 41 | 42 | public static IApplicationBuilder UseGetFile(this IApplicationBuilder app, Action configuration) 43 | { 44 | app.MapWhen(c => HttpMethod.Get.Method.Equals(c.Request.Method, StringComparison.OrdinalIgnoreCase), configuration); 45 | return app; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Dto/ResponseDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Cactus.Fileserver.Model; 5 | 6 | namespace Cactus.Fileserver.Aspnet.Dto 7 | { 8 | public class ResponseDto 9 | { 10 | 11 | public ResponseDto() { } 12 | 13 | public ResponseDto(IMetaInfo meta) 14 | { 15 | Uri = meta.Uri; 16 | Extra = meta.Extra?.ToDictionary(e => e.Key, e => e.Value); 17 | Owner = meta.Owner; 18 | Origin = meta.Origin; 19 | OriginalName = meta.OriginalName; 20 | MimeType = meta.MimeType; 21 | Icon = meta.Icon; 22 | } 23 | /// 24 | /// Not null if a error took place 25 | /// 26 | public string Error { get; set; } 27 | /// 28 | /// Full URL for getting the file. http://cdn.texas.srv.com/debug-folder/abcdf.png?x=y in our case 29 | /// 30 | public Uri Uri { get; set; } 31 | 32 | /// 33 | /// Origin URI. 34 | /// 35 | public Uri Origin { get; set; } 36 | 37 | /// 38 | /// The stored file MIME type regarding RFC6838 39 | /// In our case "image/png" because the original file was converted into PNG format during uploading 40 | /// 41 | public string MimeType { get; set; } 42 | 43 | /// 44 | /// The original uploaded file name, "pic.jpg" in our case 45 | /// 46 | public string OriginalName { get; set; } 47 | 48 | /// 49 | /// File owner. Any string that will help you to define the file owner, e-mail for example 50 | /// 51 | public string Owner { get; set; } 52 | 53 | /// 54 | /// Icon or thumbnail URI. 55 | /// 56 | public Uri Icon { get; set; } 57 | 58 | /// 59 | /// Any extra parameters. The point for lightweight extensions. 60 | /// 61 | public IDictionary Extra { get; set; } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/HttpExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http.Headers; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Cactus.Fileserver.Aspnet 6 | { 7 | internal static class HttpExtensions 8 | { 9 | public static Uri GetAbsoluteUri(this HttpRequest request) 10 | { 11 | return new Uri(string.Concat( 12 | request.Scheme, 13 | "://", 14 | request.Host.ToUriComponent(), 15 | request.PathBase.ToUriComponent(), 16 | request.Path.ToUriComponent(), 17 | request.QueryString.ToUriComponent())); 18 | } 19 | 20 | public static string GetFileName(this HttpContentHeaders headers) 21 | { 22 | return headers.ContentDisposition.FileName?.Trim('"'); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Middleware/AddFileHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Security.Principal; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Aspnet.Dto; 5 | using Cactus.Fileserver.Model; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | 10 | namespace Cactus.Fileserver.Aspnet.Middleware 11 | { 12 | /// 13 | /// Terminal handler for adding file. Put the request body into a file regardless of the content 14 | /// 15 | public class AddFileHandler 16 | { 17 | protected static readonly string JsonMimeType = "application/json"; 18 | private readonly ILogger _log; 19 | protected readonly JsonSerializerSettings SerializerSettings; 20 | 21 | public AddFileHandler(RequestDelegate next, ILogger log) 22 | { 23 | _log = log; 24 | SerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; 25 | } 26 | 27 | public async Task InvokeAsync(HttpContext ctx, IFileStorageService fileStorage) 28 | { 29 | _log.LogDebug("Store request content {content-type} into a file", ctx.Request.ContentType); 30 | var metaInfo = await ProcessUpload(ctx, fileStorage); 31 | await ResponseForUpload(ctx, metaInfo); 32 | _log.LogInformation("Served by {handler}", GetType().Name); 33 | } 34 | 35 | private async Task ResponseForUpload(HttpContext ctx, IMetaInfo metaInfo) 36 | { 37 | ctx.Response.StatusCode = (int)HttpStatusCode.Created; 38 | ctx.Response.Headers.Add("Location", metaInfo.Uri.ToString()); 39 | ctx.Response.ContentType = JsonMimeType; 40 | await ctx.Response.WriteAsync(JsonConvert.SerializeObject(BuldOkResponseObject(metaInfo), SerializerSettings)); 41 | } 42 | 43 | protected virtual async Task ProcessUpload(HttpContext ctx, IFileStorageService fileStorage) 44 | { 45 | var meta = BuildMetaInfo(ctx); 46 | await fileStorage.Create(ctx.Request.Body, meta); 47 | return meta; 48 | } 49 | 50 | protected virtual object BuldOkResponseObject(IMetaInfo meta) 51 | { 52 | return new ResponseDto(meta); 53 | } 54 | 55 | protected virtual IMetaInfo BuildMetaInfo(HttpContext ctx) 56 | { 57 | return new MetaInfo 58 | { 59 | Uri = ctx.Request.GetAbsoluteUri(), 60 | MimeType = ctx.Request.ContentType ?? "application/octet-stream", 61 | Owner = GetOwner(ctx.User.Identity) 62 | }; 63 | } 64 | 65 | /// 66 | /// Returns a string that represent file owner based on authentication context. 67 | /// By default returns Identity.Name or nul if user is not authenticated. 68 | /// 69 | /// 70 | /// Owner as a string or null 71 | protected virtual string GetOwner(IIdentity identity) 72 | { 73 | return identity?.Name; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Middleware/AddMultipartContentHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Security.Principal; 8 | using System.Threading.Tasks; 9 | using Cactus.Fileserver.Aspnet.Dto; 10 | using Cactus.Fileserver.Model; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.Extensions.Logging; 13 | using Newtonsoft.Json; 14 | 15 | namespace Cactus.Fileserver.Aspnet.Middleware 16 | { 17 | /// 18 | /// Adds files from multipart content if the request is multipart. Otherwise, continue pipeline 19 | /// 20 | public class AddFilesFromMultipartContentHandler 21 | { 22 | protected static readonly string JsonMimeType = "application/json"; 23 | private readonly RequestDelegate _next; 24 | private readonly ILogger _log; 25 | protected readonly JsonSerializerSettings SerializerSettings; 26 | 27 | public AddFilesFromMultipartContentHandler(RequestDelegate next, ILogger log) 28 | { 29 | _next = next; 30 | _log = log; 31 | SerializerSettings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore}; 32 | } 33 | 34 | public async Task InvokeAsync(HttpContext ctx, IFileStorageService fileStorage) 35 | { 36 | if (ctx.Request.ContentType?.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) ?? false) 37 | { 38 | var content = new StreamContent(ctx.Request.Body); 39 | content.Headers.ContentType = MediaTypeHeaderValue.Parse(ctx.Request.ContentType); 40 | var provider = await content.ReadAsMultipartAsync(); 41 | var resList = new List(provider.Contents.Count); 42 | foreach (var contentPart in provider.Contents) 43 | { 44 | try 45 | { 46 | var meta = await ProcessPart(ctx, contentPart, fileStorage); 47 | resList.Add(BuldOkResponseObject(meta)); 48 | } 49 | catch (Exception ex) 50 | { 51 | resList.Add(BuldErrorResponseObject(ex)); 52 | } 53 | } 54 | await ResponseForUpload(ctx, resList); 55 | _log.LogInformation("Served by {handler}", GetType().Name); 56 | } 57 | else 58 | { 59 | _log.LogInformation("Not a multipart, continue pipeline"); 60 | await _next(ctx); 61 | } 62 | } 63 | 64 | protected virtual async Task ResponseForUpload(HttpContext ctx, ICollection results) 65 | { 66 | if (results.Cast().Any(e => e.Error == null)) 67 | { 68 | ctx.Response.StatusCode = (int)HttpStatusCode.Created; 69 | ctx.Response.Headers.Add("Location", results.Cast().First(e => e.Error == null).Uri.ToString()); 70 | } 71 | else 72 | { 73 | ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 74 | } 75 | 76 | ctx.Response.ContentType = JsonMimeType; 77 | await ctx.Response.WriteAsync(JsonConvert.SerializeObject(results, SerializerSettings)); 78 | } 79 | 80 | protected virtual async Task ProcessPart(HttpContext ctx, HttpContent content, 81 | IFileStorageService fileStorage) 82 | { 83 | var meta = BuildMetaInfo(ctx, content.Headers); 84 | using (var stream = await content.ReadAsStreamAsync()) 85 | { 86 | await fileStorage.Create(stream, meta); 87 | } 88 | return meta; 89 | } 90 | 91 | protected virtual object BuldOkResponseObject(IMetaInfo meta) 92 | { 93 | return new ResponseDto(meta); 94 | } 95 | 96 | protected virtual object BuldErrorResponseObject(Exception ex) 97 | { 98 | return new ResponseDto 99 | { 100 | Error = ex.Message 101 | }; 102 | } 103 | 104 | protected virtual IMetaInfo BuildMetaInfo(HttpContext ctx, HttpContentHeaders contentHeaders) 105 | { 106 | return new MetaInfo 107 | { 108 | Uri = ctx.Request.GetAbsoluteUri(), 109 | MimeType = contentHeaders.ContentType?.ToString() ?? "application/octet-stream", 110 | OriginalName = contentHeaders.GetFileName() ?? "file", 111 | Owner = GetOwner(ctx.User.Identity) 112 | }; 113 | } 114 | 115 | /// 116 | /// Returns a string that represent file owner based on authentication context. 117 | /// By default returns Identity.Name or nul if user is not authenticated. 118 | /// 119 | /// 120 | /// Owner as a string or null 121 | protected virtual string GetOwner(IIdentity identity) 122 | { 123 | return identity?.Name; 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Middleware/DeleteFileHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Cactus.Fileserver.Aspnet.Middleware 7 | { 8 | public class DeleteFileHandler 9 | { 10 | private readonly ILogger _log; 11 | 12 | public DeleteFileHandler(RequestDelegate next, ILogger log) 13 | { 14 | _log = log; 15 | log.LogDebug(".ctor"); 16 | } 17 | 18 | public async Task InvokeAsync(HttpContext context, IFileStorageService storageService) 19 | { 20 | await storageService.Delete(context.Request.GetAbsoluteUri()); 21 | context.Response.StatusCode = (int)HttpStatusCode.NoContent; 22 | _log.LogInformation("Served by {handler}", nameof(DeleteFileHandler)); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Middleware/GetFileHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Cactus.Fileserver.Aspnet.Middleware 9 | { 10 | public class GetFileHandler 11 | { 12 | private readonly ILogger _log; 13 | 14 | public GetFileHandler(RequestDelegate next, ILogger log) 15 | { 16 | _log = log; 17 | log.LogDebug(".ctor"); 18 | } 19 | 20 | public async Task InvokeAsync(HttpContext ctx, IFileStorageService storageService) 21 | { 22 | var meta = await storageService.GetInfo(ctx.Request.GetAbsoluteUri()); 23 | var contentTask = storageService.Get(ctx.Request.GetAbsoluteUri()); 24 | ctx.Response.StatusCode = (int)HttpStatusCode.OK; 25 | ctx.Response.ContentType = meta.MimeType; 26 | if (meta.OriginalName != null) 27 | ctx.Response.Headers.Add("Content-Disposition", $"attachment;filename=UTF-8''{Uri.EscapeDataString(meta.OriginalName)}"); 28 | await (await contentTask).CopyToAsync(ctx.Response.Body); 29 | _log.LogInformation("Served by {handler}", nameof(DeleteFileHandler)); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Aspnet/Middleware/RedirectToInternalStorageHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Cactus.Fileserver.Model; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace Cactus.Fileserver.Aspnet.Middleware 7 | { 8 | public class RedirectToInternalStorageHandler 9 | { 10 | private readonly ILogger _log; 11 | 12 | public RedirectToInternalStorageHandler(RequestDelegate next, ILogger log) 13 | { 14 | _log = log; 15 | log.LogDebug(".ctor"); 16 | } 17 | 18 | public async Task InvokeAsync(HttpContext context, IFileStorageService storageService) 19 | { 20 | var info = await storageService.GetInfo(context.Request.GetAbsoluteUri()); 21 | context.Response.Redirect(info.InternalUri.ToString(), true); 22 | _log.LogInformation("Served by {handler}", nameof(RedirectToInternalStorageHandler)); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.AzureStorage/AzureFileStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | using Cactus.Fileserver.Storage; 6 | using Microsoft.Azure.Storage; 7 | using Microsoft.Azure.Storage.Blob; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace Cactus.Fileserver.AzureStorage 11 | { 12 | public class AzureFileStorage : IFileStorage 13 | { 14 | private readonly string _containerName; 15 | private readonly IStoredNameProvider _nameProvider; 16 | private readonly ILogger _log; 17 | private readonly string _cacheControl; 18 | private readonly string _connectionString; 19 | private CloudBlobContainer _cloudBlobContainer; 20 | 21 | public AzureFileStorage(string connectionString, string containerName, IStoredNameProvider nameProvider, ILogger log) 22 | { 23 | _containerName = containerName; 24 | _nameProvider = nameProvider; 25 | _log = log; 26 | //// sec * min * hour 27 | _cacheControl = $"max-age={60 * 60 * 24}, must-revalidate"; 28 | _connectionString = connectionString; 29 | try 30 | { 31 | InitStorage().Wait(); 32 | } 33 | catch (Exception e) 34 | { 35 | _log.LogError("Init failed {0}: {1}", e.GetType().Name, e.Message); 36 | } 37 | } 38 | 39 | public async Task Add(Stream stream, IMetaInfo info) 40 | { 41 | await InitStorage().ConfigureAwait(false); 42 | var targetFile = _nameProvider.GetName(info); 43 | _log.LogDebug("Writing {0} azure", targetFile); 44 | var blockBlob = _cloudBlobContainer.GetBlockBlobReference(targetFile); 45 | blockBlob.Properties.ContentType = info.MimeType; 46 | blockBlob.Properties.CacheControl = _cacheControl; 47 | 48 | if (!info.MimeType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) 49 | { 50 | blockBlob.Properties.ContentDisposition = 51 | $"attachment;filename=UTF-8''{Uri.EscapeDataString(info.OriginalName)}"; 52 | } 53 | 54 | if (info.Extra != null) 55 | { 56 | foreach (var kvp in info.Extra) 57 | { 58 | blockBlob.Metadata.Add(kvp); 59 | } 60 | } 61 | 62 | await blockBlob.UploadFromStreamAsync(stream); 63 | return blockBlob.Uri; 64 | } 65 | 66 | public async Task Delete(IMetaInfo metaInfo) 67 | { 68 | await InitStorage().ConfigureAwait(false); 69 | var targetFile = metaInfo.InternalUri.GetResource(); 70 | _log.LogDebug("Delete {0} from azure", targetFile); 71 | var blockBlob = _cloudBlobContainer.GetBlockBlobReference(targetFile); 72 | await blockBlob.DeleteIfExistsAsync().ConfigureAwait(false); 73 | } 74 | 75 | public async Task Get(IMetaInfo metaInfo) 76 | { 77 | await InitStorage().ConfigureAwait(false); 78 | var targetFile = metaInfo.InternalUri.GetResource(); 79 | var blob = _cloudBlobContainer.GetBlockBlobReference(targetFile); 80 | return await blob.OpenReadAsync().ConfigureAwait(false); 81 | } 82 | 83 | private async Task InitStorage() 84 | { 85 | if (_cloudBlobContainer == null) 86 | { 87 | try 88 | { 89 | var storageAccount = CloudStorageAccount.Parse(_connectionString); 90 | var blobClient = storageAccount.CreateCloudBlobClient(); 91 | 92 | ////Set up DefaultServiceVersion to support Content-Disposition header 93 | var serviceProperties = await blobClient.GetServicePropertiesAsync().ConfigureAwait(false); 94 | serviceProperties.DefaultServiceVersion = "2013-08-15"; 95 | await blobClient.SetServicePropertiesAsync(serviceProperties).ConfigureAwait(false); 96 | 97 | _cloudBlobContainer = blobClient.GetContainerReference(_containerName); 98 | await _cloudBlobContainer.CreateIfNotExistsAsync().ConfigureAwait(false); 99 | await _cloudBlobContainer.SetPermissionsAsync( 100 | new BlobContainerPermissions 101 | { 102 | PublicAccess = BlobContainerPublicAccessType.Blob 103 | }).ConfigureAwait(false); 104 | } 105 | catch 106 | { 107 | _cloudBlobContainer = null; 108 | throw; 109 | } 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Cactus.Fileserver.AzureStorage/Cactus.Fileserver.AzureStorage.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Cactus.Fileserver.AzureStorage 6 | Cactus.Fileserver.AzureStorage 7 | 3.0 8 | yan.oreshchenkov 9 | CactusSoft LLC 10 | Azure storage for Cactus.Fileserver 11 | CactusSoft LLC 12 | https://github.com/CactusSoft/Cactus.Fileserver/blob/master/LICENSE 13 | https://github.com/CactusSoft/Cactus.Fileserver 14 | https://github.com/CactusSoft/Cactus.Fileserver 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/Assembly.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Cactus.Fileserver.Tests")] 4 | -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/Cactus.Fileserver.ImageResizer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | true 6 | 4.0.5 7 | Yan O 8 | CactusSoft LLC 9 | Image resizing module for Cactus.Fileserver 10 | CactusSoft LLC 11 | 12 | https://github.com/CactusSoft/Cactus.Fileserver 13 | https://github.com/CactusSoft/Cactus.Fileserver 14 | MIT 15 | 4.0.9 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Cactus.Fileserver.ImageResizer 6 | { 7 | public static class ConfigurationExtensions 8 | { 9 | public static IServiceCollection AddDynamicResizing(this IServiceCollection services, Action configureOptions) 10 | { 11 | services.Configure(configureOptions); 12 | services.AddSingleton(); 13 | return services; 14 | } 15 | 16 | public static IApplicationBuilder UseDynamicResizing(this IApplicationBuilder app) 17 | { 18 | return app.UseMiddleware(); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/DynamicResizeMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.ImageResizer.Utils; 5 | using Cactus.Fileserver.Model; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Cactus.Fileserver.ImageResizer 10 | { 11 | public class DynamicResizingMiddleware 12 | { 13 | 14 | private readonly RequestDelegate _next; 15 | private readonly ILogger _log; 16 | 17 | public DynamicResizingMiddleware(RequestDelegate next, ILogger logger) 18 | { 19 | _next = next; 20 | _log = logger; 21 | } 22 | 23 | public async Task InvokeAsync(HttpContext context, IImageResizerService resizer, IFileStorageService storage) 24 | { 25 | if (!context.Request.QueryString.HasValue) 26 | { 27 | _log.LogDebug("No query string, no dynamic resizing"); 28 | await _next(context); 29 | return; 30 | } 31 | 32 | var instructions = new ResizeInstructions(context.Request.QueryString); 33 | if (instructions.Width == null && instructions.Height == null) 34 | { 35 | _log.LogDebug("No resizing instruction found in query string, continue pipeline"); 36 | await _next(context); 37 | return; 38 | } 39 | 40 | _log.LogDebug("It looks like resizing is requested. Looking for the file meta data first..."); 41 | var request = context.Request; 42 | MetaInfo metaData; 43 | try 44 | { 45 | metaData = await storage.GetInfo(request.GetAbsoluteUri()); 46 | _ = metaData ?? throw new FileNotFoundException($"No metadata found for {request.GetAbsoluteUri()}"); 47 | 48 | if (metaData.Origin != null && !metaData.Uri.Equals(metaData.Origin)) 49 | { 50 | _log.LogDebug("{uri} is not origin, getting the origin for transformation", metaData.Uri); 51 | metaData = await storage.GetInfo(metaData.Origin); 52 | } 53 | } 54 | catch (FileNotFoundException) 55 | { 56 | _log.LogWarning("No metadata found for the requested file, continue pipeline"); 57 | await _next(context); 58 | return; 59 | } 60 | 61 | _log.LogDebug("Metadata found, let's see if we can resize it"); 62 | if (!metaData.MimeType.Equals("image/jpeg") && 63 | !metaData.MimeType.Equals("image/jpg") && 64 | !metaData.MimeType.Equals("image/png")) 65 | { 66 | _log.LogInformation("The file type is {content-type}, resizing is not supported, continue pipeline", metaData.MimeType); 67 | await _next(context); 68 | return; 69 | } 70 | 71 | _log.LogDebug("Let's try to find already resized version"); 72 | var sizeKey = instructions.BuildSizeKey(); 73 | if (metaData.Extra.TryGetValue(sizeKey, out var redirectUri)) 74 | { 75 | _log.LogDebug("{size_key} size found, do redirect", sizeKey); 76 | context.Response.Redirect(redirectUri, true); 77 | _log.LogInformation("Served by {handler}", GetType().Name); 78 | return; 79 | } 80 | 81 | try 82 | { 83 | _log.LogDebug("Do resizing"); 84 | using (var tempFile = new MemoryStream()) 85 | using (var original = await storage.Get(metaData.Uri)) 86 | { 87 | resizer.Resize(original, tempFile, instructions); 88 | var newFileInfo = new MetaInfo(metaData) { Origin = metaData.Uri, Uri = metaData.Uri.GetFolder() }; 89 | tempFile.Position = 0; 90 | await storage.Create(tempFile, newFileInfo); 91 | metaData.Extra.Add(sizeKey, newFileInfo.Uri.ToString()); 92 | await storage.UpdateInfo(metaData); 93 | _log.LogDebug("Resized successfully to {size_key}, redirect to {url}", sizeKey, newFileInfo.Uri); 94 | context.Response.Redirect(newFileInfo.Uri.ToString(), true); 95 | _log.LogInformation("Served by {handler}", GetType().Name); 96 | return; 97 | } 98 | } 99 | catch (Exception ex) 100 | { 101 | _log.LogError("Exception during dynamic resizing, redirect to the origin {0}", ex); 102 | context.Response.Redirect(metaData.Uri.ToString(), false); 103 | } 104 | 105 | await _next(context); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/HttpRequestExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Cactus.Fileserver.ImageResizer 5 | { 6 | internal static class HttpRequestExtension 7 | { 8 | public static Uri GetAbsoluteUri(this HttpRequest request) 9 | { 10 | return new Uri(string.Concat( 11 | request.Scheme, 12 | "://", 13 | request.Host.ToUriComponent(), 14 | request.PathBase.ToUriComponent(), 15 | request.Path.ToUriComponent(), 16 | request.QueryString.ToUriComponent())); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/ImageResizerService.cs: -------------------------------------------------------------------------------- 1 | using Cactus.Fileserver.ImageResizer.Utils; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using SixLabors.ImageSharp; 5 | using SixLabors.ImageSharp.Processing; 6 | using System; 7 | using System.IO; 8 | using Image = SixLabors.ImageSharp.Image; 9 | 10 | 11 | namespace Cactus.Fileserver.ImageResizer 12 | { 13 | public interface IImageResizerService 14 | { 15 | /// 16 | /// Apply instructions to an image. 17 | /// A good point to extra configuration of Image Resizer 18 | /// 19 | /// Input image stream 20 | /// Output stream to write the result 21 | /// Instructions to apply 22 | /// Result image as a stream. Caller have to care about the stream disposing. 23 | void Resize(Stream inputStream, Stream outputStream, ResizeInstructions instructions); 24 | 25 | (int Width, int Height, bool isResizable) Probe(Stream stream); 26 | } 27 | 28 | public class ImageResizerService : IImageResizerService 29 | { 30 | private readonly IOptionsMonitor _optionsMonitor; 31 | private readonly ILogger _log; 32 | 33 | public ImageResizerService(IOptionsMonitor optionsMonitor, ILogger log) 34 | { 35 | _optionsMonitor = optionsMonitor; 36 | _log = log; 37 | } 38 | 39 | public virtual void Resize(Stream inputStream, Stream outputStream, ResizeInstructions instructions) 40 | { 41 | if (!inputStream.CanRead) 42 | throw new ArgumentException("inputStream is nor readable"); 43 | if (!outputStream.CanWrite) 44 | throw new ArgumentException("outputStream is nor writable"); 45 | var options = _optionsMonitor.CurrentValue; 46 | instructions.Join(options.DefaultInstructions); 47 | instructions.Join(options.MandatoryInstructions, true); 48 | 49 | using (var image = Image.Load(inputStream, out var imageInfo)) 50 | { 51 | var targetSize = GetTargetSize(instructions, image.Width / (double)image.Height); 52 | _log.LogDebug("Resize {image_format_name}, original size {width}x{height}:{size} bytes, target size {width}x{height}", imageInfo.Name, image.Width, image.Height, inputStream.Length, targetSize.Width, targetSize.Height); 53 | image.Mutate(x => x.Resize(targetSize.Width, targetSize.Height)); 54 | image.Save(outputStream, imageInfo); // Automatic encoder selected based on extension. 55 | _log.LogDebug("Resizing complete, output image size: {width}x{height}:{size} bytes", targetSize.Width, targetSize.Height, outputStream.Length); 56 | } 57 | } 58 | 59 | public (int Width, int Height, bool isResizable) Probe(Stream stream) 60 | { 61 | var info = Image.Identify(stream); 62 | return (info.Width, info.Height, true); //For now we consider everything as resizable 63 | } 64 | 65 | internal static (int Width, int Height) GetTargetSize(ResizeInstructions instructions, double imageRatio) 66 | { 67 | var targetWidth = Math.Min(instructions.Width ?? -1, instructions.MaxWidth); 68 | var targetHeight = Math.Min(instructions.Height ?? -1, instructions.MaxHeight); 69 | 70 | if (targetWidth == -1 && targetHeight == -1) 71 | throw new ArgumentException("Nigher width or height are defined"); 72 | 73 | if (targetHeight == -1 || targetWidth == -1 || (instructions.KeepAspectRatio.HasValue && instructions.KeepAspectRatio.Value)) 74 | { 75 | if (targetHeight != -1 && targetWidth != -1) 76 | { 77 | if (imageRatio > 1) 78 | targetHeight = (int)Math.Round(targetWidth / imageRatio); 79 | else 80 | targetWidth = (int)Math.Round(targetHeight * imageRatio); 81 | } 82 | else if (targetHeight != -1) 83 | { 84 | targetWidth = (int)Math.Round(targetHeight * imageRatio); 85 | } 86 | else if (targetWidth != -1) 87 | { 88 | targetHeight = (int)Math.Round(targetWidth / imageRatio); 89 | } 90 | else 91 | { 92 | //That's generally impossible because of the same check above, but let's check it twice 93 | throw new ArgumentException("Nigher width or height are defined"); 94 | } 95 | } 96 | return (targetWidth, targetHeight); 97 | } 98 | } 99 | 100 | public class ResizingOptions 101 | { 102 | public ResizeInstructions DefaultInstructions { get; set; } 103 | public ResizeInstructions MandatoryInstructions { get; set; } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Cactus.Fileserver.ImageResizer/Utils/ResizeInstructions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Cactus.Fileserver.ImageResizer.Utils 5 | { 6 | public class ResizeInstructions 7 | { 8 | public ResizeInstructions() 9 | { 10 | MaxWidth = 4096; 11 | MaxHeight = 4096; //4k by any dimension 12 | } 13 | 14 | public ResizeInstructions(QueryString queryString) 15 | { 16 | if (!queryString.HasValue) 17 | return; 18 | var qString = queryString.Value; 19 | if (qString[0] == '?' && qString.Length > 1) 20 | qString = qString.Substring(1); 21 | 22 | foreach (var param in qString.Split('&')) 23 | { 24 | var kvp = param.Split('='); 25 | if (kvp.Length == 2) 26 | { 27 | if (nameof(Width).Equals(kvp[0], StringComparison.OrdinalIgnoreCase)) 28 | { 29 | if (int.TryParse(kvp[1], out var v) && v > 0) 30 | Width = v; 31 | } 32 | else if (nameof(Height).Equals(kvp[0], StringComparison.OrdinalIgnoreCase)) 33 | { 34 | if (int.TryParse(kvp[1], out var v) && v > 0) 35 | Height = v; 36 | } 37 | else if (nameof(KeepAspectRatio).Equals(kvp[0], StringComparison.OrdinalIgnoreCase)) 38 | { 39 | if (bool.TryParse(kvp[1], out var v)) 40 | KeepAspectRatio = v; 41 | } 42 | } 43 | } 44 | } 45 | 46 | public int? Width { get; set; } 47 | public int? Height { get; set; } 48 | public int MaxWidth { get; set; } 49 | public int MaxHeight { get; set; } 50 | public bool? KeepAspectRatio { get; set; } 51 | 52 | public void Join(ResizeInstructions instructions, bool @override = false) 53 | { 54 | if (instructions == null) return; 55 | if (instructions.Width.HasValue && (!Width.HasValue || @override)) 56 | Width = instructions.Width; 57 | 58 | if (instructions.Height.HasValue && (!Height.HasValue || @override)) 59 | Height = instructions.Height; 60 | 61 | if (instructions.KeepAspectRatio.HasValue && (!KeepAspectRatio.HasValue || @override)) 62 | KeepAspectRatio = instructions.KeepAspectRatio; 63 | 64 | if (@override) 65 | { 66 | MaxWidth = instructions.MaxWidth; 67 | MaxHeight = instructions.MaxHeight; 68 | } 69 | } 70 | } 71 | 72 | public static class ResizeInstructionsExtensions 73 | { 74 | public static string BuildSizeKey(this ResizeInstructions instructions) 75 | { 76 | return "alt_size_" 77 | + (instructions.Width?.ToString() ?? "-") 78 | + 'x' 79 | + (instructions.Height?.ToString() ?? "-"); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage.Test/AllInTheSameFolderUriResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Cactus.Fileserver.LocalStorage.Config; 4 | using Cactus.Fileserver.Model; 5 | using Microsoft.Extensions.Options; 6 | using NUnit.Framework; 7 | 8 | namespace Cactus.Fileserver.LocalStorage.Test 9 | { 10 | public class AllInTheSameFolderUriResolverTests 11 | { 12 | [Test] 13 | public void ResolveUriTest() 14 | { 15 | var baseUri = "http://some.somewhere/folder"; 16 | var baseFolder = Path.GetTempPath(); 17 | var fileName = "somefile.ext"; 18 | var fullFilePath = Path.Combine(baseFolder, fileName); 19 | var options = Options.Create(new LocalFileStorageOptions 20 | { 21 | BaseFolder = baseFolder, 22 | BaseUri = new Uri(baseUri) 23 | }); 24 | var resolver = new SingleFolderUriResolver(options); 25 | 26 | var res = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + fullFilePath) }); 27 | Assert.AreEqual(baseUri + "/" + fileName, res.ToString()); 28 | 29 | fileName = "withoutextension"; 30 | fullFilePath = Path.Combine(baseFolder, fileName); 31 | res = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + fullFilePath) }); 32 | Assert.AreEqual(baseUri + "/" + fileName, res.ToString()); 33 | 34 | Assert.Throws(() => resolver.ResolveUri(null)); 35 | Assert.Throws(() => resolver.ResolveUri(new MetaInfo())); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage.Test/Cactus.Fileserver.LocalStorage.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage.Test/FolderUriResolverTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Cactus.Fileserver.LocalStorage.Config; 4 | using Cactus.Fileserver.Model; 5 | using Microsoft.Extensions.Options; 6 | using NUnit.Framework; 7 | 8 | namespace Cactus.Fileserver.LocalStorage.Test 9 | { 10 | [TestFixture] 11 | public class FolderUriResolverTest 12 | { 13 | [Test] 14 | public void ResolvePathTest() 15 | { 16 | var baseUrl = "http://localhost:5000"; 17 | var options = Options.Create(new LocalFileStorageOptions 18 | { 19 | BaseFolder = Path.GetTempPath(), 20 | BaseUri = new Uri(baseUrl) 21 | }); 22 | 23 | var resolver = new SubfolderUriResolver(options); 24 | 25 | var path = resolver.ResolvePath(new MetaInfo { Uri = options.Value.BaseUri }); 26 | Assert.AreEqual(options.Value.BaseFolder, path); 27 | 28 | path = resolver.ResolvePath(new MetaInfo { Uri = new Uri(baseUrl + "/folder1") }); 29 | Assert.AreEqual(Path.Combine(options.Value.BaseFolder, "folder1"), path); 30 | 31 | path = resolver.ResolvePath(new MetaInfo { Uri = new Uri(baseUrl + "/folder1/folder2") }); 32 | Assert.AreEqual(Path.Combine(options.Value.BaseFolder, "folder1", "folder2"), path); 33 | 34 | path = resolver.ResolvePath(new MetaInfo { Uri = new Uri(baseUrl + "/folder1/folder2?param=value&x=y") }); 35 | Assert.AreEqual(Path.Combine(options.Value.BaseFolder, "folder1", "folder2"), path); 36 | } 37 | 38 | [Test] 39 | public void ResolveUriTest() 40 | { 41 | var baseUrl = "http://localhost:5000"; 42 | var options = Options.Create(new LocalFileStorageOptions 43 | { 44 | BaseFolder = Path.GetTempPath(), 45 | BaseUri = new Uri(baseUrl) 46 | }); 47 | 48 | var resolver = new SubfolderUriResolver(options); 49 | 50 | var uri = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + Path.Combine(options.Value.BaseFolder, "file.ext")) }); 51 | Assert.AreEqual(new Uri(baseUrl + "/file.ext"), uri); 52 | 53 | uri = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + Path.Combine(options.Value.BaseFolder, "folder1", "file.ext")) }); 54 | Assert.AreEqual(new Uri(baseUrl + "/folder1/file.ext"), uri); 55 | 56 | uri = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + Path.Combine(options.Value.BaseFolder, "folder1", "folder2", "file.ext")) }); 57 | Assert.AreEqual(new Uri(baseUrl + "/folder1/folder2/file.ext"), uri); 58 | 59 | uri = resolver.ResolveUri(new MetaInfo { InternalUri = new Uri("file://" + Path.Combine(options.Value.BaseFolder, "folder1", "folder2", "file.ext")+"?query=val") }); 60 | Assert.AreEqual(new Uri(baseUrl + "/folder1/folder2/file.ext"), uri); 61 | 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage.Test/LocalFileStorageTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Cactus.Fileserver.Model; 7 | using Cactus.Fileserver.Storage; 8 | using Microsoft.Extensions.Logging.Abstractions; 9 | using Moq; 10 | using NUnit.Framework; 11 | 12 | namespace Cactus.Fileserver.LocalStorage.Test 13 | { 14 | public class LocalFileStorageTests 15 | { 16 | [Test] 17 | public async Task CrudTest() 18 | { 19 | //Arrange 20 | var baseUri = "http://some.somewhere/folder"; 21 | var baseFolder = Path.GetTempPath(); 22 | var fileName = Path.GetRandomFileName(); 23 | var metaInfo = new MetaInfo 24 | { 25 | OriginalName = fileName, 26 | Extra = new Dictionary { { "key", "value" } }, 27 | MimeType = "octer/stream", 28 | }; 29 | 30 | var nameGeneratorMock = new Mock(); 31 | nameGeneratorMock.Setup(e => e.GetName(It.IsAny())).Returns(meta => meta.OriginalName); 32 | 33 | var uriResolverMock = new Mock(); 34 | uriResolverMock.Setup(e => e.ResolvePath(It.IsAny())).Returns(baseFolder); 35 | uriResolverMock.Setup(e => e.ResolveUri(It.IsAny())) 36 | .Returns(meta => new Uri(baseUri + '/' + meta.InternalUri.GetResource())); 37 | 38 | var storage = new LocalFileStorage(nameGeneratorMock.Object, uriResolverMock.Object, NullLogger.Instance); 39 | await using (var stream = new MemoryStream()) 40 | { 41 | stream.Write(Encoding.ASCII.GetBytes("hello world")); 42 | stream.Seek(0, 0); 43 | 44 | //Act 45 | await storage.Add(stream, metaInfo); 46 | Assert.NotNull(metaInfo.InternalUri); 47 | Assert.NotNull(metaInfo.Uri); 48 | Assert.IsTrue(File.Exists(Path.Combine(baseFolder, fileName))); 49 | nameGeneratorMock.VerifyAll(); 50 | 51 | await using var resStream = await storage.Get(metaInfo); 52 | Assert.IsNotNull(resStream); 53 | Assert.AreEqual(stream.Length, resStream.Length); 54 | Assert.IsTrue(File.Exists(Path.Combine(baseFolder, fileName))); 55 | } 56 | 57 | await storage.Delete(metaInfo); 58 | Assert.IsFalse(File.Exists(Path.Combine(baseFolder, fileName))); 59 | uriResolverMock.VerifyAll(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage.Test/LocalMetaInfoStorageTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Cactus.Fileserver.LocalStorage.Config; 6 | using Cactus.Fileserver.Model; 7 | using Microsoft.Extensions.Logging.Abstractions; 8 | using Microsoft.Extensions.Options; 9 | using NUnit.Framework; 10 | 11 | namespace Cactus.Fileserver.LocalStorage.Test 12 | { 13 | public class LocalMetaInfoStorageTests 14 | { 15 | [Test] 16 | public async Task CrudTest() 17 | { 18 | var metafileExt = ".json"; 19 | var baseFolder = Path.GetTempPath(); 20 | var fileName = Path.GetRandomFileName(); 21 | var options = Options.Create(new LocalMetaStorageOptions 22 | { 23 | BaseFolder = baseFolder 24 | }); 25 | var metaStorage = new LocalMetaInfoStorage(options, NullLogger.Instance); 26 | var metaInfo = new MetaInfo 27 | { 28 | InternalUri = new Uri("file://" + baseFolder + '/' + fileName), 29 | Uri = new Uri("http://some.somewhere/folder/" + fileName), 30 | Extra = new Dictionary { { "key", "value" } }, 31 | MimeType = "octer/stream", 32 | }; 33 | 34 | await metaStorage.Add(metaInfo); 35 | Assert.IsTrue(File.Exists(Path.Combine(baseFolder, fileName + metafileExt))); 36 | 37 | metaInfo.Extra.Add("updated", "-"); 38 | await metaStorage.Update(metaInfo); 39 | Assert.IsTrue(File.Exists(Path.Combine(baseFolder, fileName + metafileExt))); 40 | 41 | var metaInfoReceived = await metaStorage.Get(metaInfo.Uri); 42 | Assert.IsTrue(metaInfoReceived.Extra.ContainsKey("updated")); 43 | 44 | await metaStorage.Delete(metaInfo.Uri); 45 | Assert.IsFalse(File.Exists(Path.Combine(baseFolder, fileName + metafileExt))); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/Assembly.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Cactus.Fileserver.LocalStorage.Test")] 4 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/Cactus.Fileserver.LocalStorage.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Cactus.Fileserver.LocalStorage 6 | Cactus.Fileserver.LocalStorage 7 | 4.0.4 8 | CactusSoft LLC 9 | YanO 10 | Copyright CactusSoft 2016 11 | https://github.com/CactusSoft/Cactus.Fileserver 12 | 13 | Local filesystem storage implementation for Cactus.Fileserver 14 | true 15 | 4.0.0.0 16 | 4.0.0.0 17 | MIT 18 | https://github.com/CactusSoft/Cactus.Fileserver 19 | GitHub 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/Config/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cactus.Fileserver.Storage; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Cactus.Fileserver.LocalStorage.Config 8 | { 9 | public static class ConfigurationExtensions 10 | { 11 | public static IServiceCollection AddLocalFileStorage(this IServiceCollection services, Action configureOptions) 12 | { 13 | services.Configure(configureOptions); 14 | services.TryAddEnumerable(ServiceDescriptor.Singleton, LocalFileStorageOptionsValidator>()); 15 | 16 | services.AddScoped(); 17 | services.AddScoped(); 18 | services.AddSingleton(); 19 | services.AddScoped(); 20 | return services; 21 | } 22 | 23 | public static IServiceCollection AddLocalMetaStorage(this IServiceCollection services, Action configureOptions) 24 | { 25 | services.Configure(configureOptions); 26 | services.TryAddEnumerable(ServiceDescriptor.Singleton, LocalMetaStorageOptionsValidator>()); 27 | 28 | services.AddScoped(); 29 | return services; 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/Config/LocalMetaStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace Cactus.Fileserver.LocalStorage.Config 5 | { 6 | public class LocalMetaStorageOptions 7 | { 8 | public LocalMetaStorageOptions() 9 | { 10 | Extension = ".json"; 11 | } 12 | 13 | public string BaseFolder { get; set; } 14 | public string Extension { get; set; } 15 | } 16 | 17 | public class LocalMetaStorageOptionsValidator : IValidateOptions 18 | { 19 | public ValidateOptionsResult Validate(string name, LocalMetaStorageOptions options) 20 | { 21 | if (options.BaseFolder == null) return ValidateOptionsResult.Fail("BaseFolder cannot be null"); 22 | if (!Directory.Exists(options.BaseFolder)) return ValidateOptionsResult.Fail($"Path {options.BaseFolder} doesn't exist"); 23 | return ValidateOptionsResult.Success; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/Config/LocalStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace Cactus.Fileserver.LocalStorage.Config 6 | { 7 | public class LocalFileStorageOptions 8 | { 9 | public string BaseFolder { get; set; } 10 | public Uri BaseUri { get; set; } 11 | } 12 | 13 | public class LocalFileStorageOptionsValidator : IValidateOptions 14 | { 15 | public ValidateOptionsResult Validate(string name, LocalFileStorageOptions options) 16 | { 17 | if (options.BaseUri == null) return ValidateOptionsResult.Fail("BaseUri cannot be null"); 18 | if (options.BaseFolder == null) return ValidateOptionsResult.Fail("BaseFolder cannot be null"); 19 | if (!Directory.Exists(options.BaseFolder)) return ValidateOptionsResult.Fail($"Path {options.BaseFolder} doesn't exist"); 20 | return ValidateOptionsResult.Success; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/LocalFileStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | using Cactus.Fileserver.Storage; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Cactus.Fileserver.LocalStorage 9 | { 10 | public class LocalFileStorage : IFileStorage 11 | { 12 | private const int MaxTriesCount = 10; 13 | 14 | private readonly IStoredNameProvider _nameProvider; 15 | private readonly IUriResolver _uriResolver; 16 | private readonly ILogger _log; 17 | 18 | public LocalFileStorage(IStoredNameProvider nameProvider, IUriResolver uriResolver, ILogger log) 19 | { 20 | _nameProvider = nameProvider; 21 | _uriResolver = uriResolver; 22 | _log = log; 23 | } 24 | 25 | public async Task Add(Stream stream, IMetaInfo info) 26 | { 27 | _log.LogDebug("Adding {filename}", info.OriginalName); 28 | var filename = _nameProvider.GetName(info); 29 | var path = _uriResolver.ResolvePath(info); 30 | var fullFilePath = Path.Combine(path, filename); 31 | var triesCount = 0; 32 | for (; File.Exists(fullFilePath) && triesCount < MaxTriesCount; triesCount++) 33 | { 34 | await Task.Delay(42); // you know, the answer to the question of life 35 | filename = _nameProvider.Regenerate(info, filename); 36 | fullFilePath = Path.Combine(path, filename); 37 | } 38 | 39 | if (triesCount > MaxTriesCount) 40 | throw new IOException("Could not generate unique file name"); 41 | 42 | using (var dest = new FileStream(fullFilePath, FileMode.CreateNew)) 43 | { 44 | await stream.CopyToAsync(dest); 45 | } 46 | 47 | info.InternalUri = new Uri("file://" + fullFilePath); 48 | info.Uri = _uriResolver.ResolveUri(info); 49 | _log.LogDebug("{filename} stored as {file}, the full uri is {uri}", info.OriginalName, fullFilePath, info.Uri); 50 | return info.Uri; 51 | } 52 | 53 | public Task Delete(IMetaInfo fileInfo) 54 | { 55 | _log.LogDebug("Delete file {name}: {uri}", fileInfo.OriginalName, fileInfo.InternalUri.AbsolutePath); 56 | File.Delete(fileInfo.InternalUri.AbsolutePath); 57 | return Task.CompletedTask; 58 | } 59 | 60 | public Task Get(IMetaInfo fileInfo) 61 | { 62 | _log.LogDebug("Read file {name}: {uri}", fileInfo.OriginalName, fileInfo.InternalUri.AbsolutePath); 63 | var fullFilePath = fileInfo.InternalUri.AbsolutePath; 64 | var stream = new FileStream(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); 65 | return Task.FromResult((Stream)stream); 66 | } 67 | } 68 | 69 | 70 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/LocalMetaInfoStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.LocalStorage.Config; 5 | using Cactus.Fileserver.Model; 6 | using Cactus.Fileserver.Storage; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.Options; 9 | using Newtonsoft.Json; 10 | 11 | namespace Cactus.Fileserver.LocalStorage 12 | { 13 | public class LocalMetaInfoStorage : IMetaInfoStorage 14 | { 15 | private readonly string _baseFolder; 16 | private readonly string _metafileExt; 17 | private readonly ILogger _log; 18 | 19 | public LocalMetaInfoStorage(IOptions settings, ILogger log) 20 | { 21 | _log = log; 22 | _baseFolder = settings.Value.BaseFolder; 23 | _metafileExt = settings.Value.Extension; 24 | } 25 | 26 | public async Task Add(T info) where T : IMetaInfo 27 | { 28 | var fullFilename = GetFile(info.Uri); 29 | _log.LogDebug("Write metainfo to {file}", fullFilename); 30 | using (var writer = new StreamWriter(File.Create(fullFilename))) 31 | { 32 | await writer.WriteAsync(JsonConvert.SerializeObject(info)); 33 | } 34 | } 35 | 36 | public Task Update(T info) where T : IMetaInfo 37 | { 38 | return Add(info); 39 | } 40 | 41 | public Task Delete(Uri uri) 42 | { 43 | var fullFilename = GetFile(uri); 44 | _log.LogDebug("Delete metainfo {file}", fullFilename); 45 | File.Delete(fullFilename); 46 | return Task.CompletedTask; 47 | } 48 | 49 | public async Task Get(Uri uri) where T : IMetaInfo 50 | { 51 | var file = GetFile(uri); 52 | _log.LogDebug("Get metainfo from {file}", file); 53 | using (var reader = new StreamReader(new FileStream(file, FileMode.Open))) 54 | { 55 | return JsonConvert.DeserializeObject(await reader.ReadToEndAsync()); 56 | } 57 | } 58 | 59 | /// 60 | /// Returns full file path to the metainfo file 61 | /// 62 | /// 63 | /// 64 | protected virtual string GetFile(Uri uri) 65 | { 66 | var fileName = uri.GetResource(); 67 | if (!string.IsNullOrWhiteSpace(_metafileExt)) fileName += _metafileExt; 68 | return Path.Combine(_baseFolder, fileName); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/SingleFolderUriResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cactus.Fileserver.LocalStorage.Config; 3 | using Cactus.Fileserver.Model; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Cactus.Fileserver.LocalStorage 7 | { 8 | /// 9 | /// Direct all requests to store files into base folder 10 | /// 11 | public class SingleFolderUriResolver : IUriResolver 12 | { 13 | private readonly string _baseFolder; 14 | private readonly string _baseUri; 15 | 16 | public SingleFolderUriResolver(IOptions settings) 17 | { 18 | _baseUri = settings.Value.BaseUri.ToString().TrimEnd('/'); 19 | _baseFolder = settings.Value.BaseFolder; 20 | } 21 | 22 | public Uri ResolveUri(IMetaInfo info) 23 | { 24 | _ = info?.InternalUri ?? throw new ArgumentNullException(nameof(IMetaInfo) + '.' + nameof(IMetaInfo.InternalUri)); 25 | var fileName = info.InternalUri.GetResource(); 26 | return new Uri(_baseUri + '/' + fileName); 27 | } 28 | 29 | public string ResolvePath(IMetaInfo info) 30 | { 31 | return _baseFolder; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/SubfolderUriResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Cactus.Fileserver.LocalStorage.Config; 5 | using Cactus.Fileserver.Model; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Cactus.Fileserver.LocalStorage 9 | { 10 | /// 11 | /// Direct storing files in subfolders of the baseFolder based on upload URI 12 | /// 13 | public class SubfolderUriResolver : IUriResolver 14 | { 15 | private readonly string _baseFolder; 16 | private readonly string _baseUri; 17 | private static readonly char UriPathSeparator = '/'; 18 | 19 | public SubfolderUriResolver(IOptions settings) 20 | { 21 | _baseUri = settings.Value.BaseUri.ToString().TrimEnd(UriPathSeparator); 22 | _baseFolder = settings.Value.BaseFolder; 23 | } 24 | 25 | public Uri ResolveUri(IMetaInfo info) 26 | { 27 | _ = info?.InternalUri ?? throw new ArgumentNullException(nameof(IMetaInfo) + '.' + nameof(IMetaInfo.InternalUri)); 28 | var path = info.InternalUri.AbsolutePath; 29 | var basePath = _baseFolder; 30 | if (Path.DirectorySeparatorChar != UriPathSeparator) 31 | { 32 | basePath = basePath.Replace(Path.DirectorySeparatorChar, UriPathSeparator); 33 | } 34 | if (path.StartsWith(basePath)) 35 | { 36 | var subPath = path.Substring(_baseFolder.Length); 37 | var res = new Uri(_baseUri + UriPathSeparator + subPath.TrimStart(UriPathSeparator)); 38 | return res; 39 | } 40 | 41 | return new Uri(_baseUri + UriPathSeparator + info.InternalUri.GetResource()); 42 | } 43 | 44 | public string ResolvePath(IMetaInfo info) 45 | { 46 | _ = info?.Uri ?? throw new ArgumentNullException(nameof(IMetaInfo) + '.' + nameof(IMetaInfo.Uri)); 47 | if (info.Uri.AbsolutePath != "/") 48 | { 49 | var path = info.Uri.AbsolutePath.Trim(UriPathSeparator) 50 | .Split(UriPathSeparator) 51 | .Aggregate(_baseFolder, Path.Combine); 52 | 53 | if (!Directory.Exists(path)) 54 | { 55 | Directory.CreateDirectory(path); 56 | } 57 | 58 | return path; 59 | } 60 | return _baseFolder; 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.LocalStorage/UriResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cactus.Fileserver.Model; 3 | 4 | namespace Cactus.Fileserver.LocalStorage 5 | { 6 | public interface IUriResolver 7 | { 8 | /// 9 | /// Returns externally accessible uri to GET the file 10 | /// 11 | /// 12 | Uri ResolveUri(IMetaInfo info); 13 | 14 | /// 15 | /// Returns path to a folder where the file is or where it should be added to 16 | /// 17 | /// 18 | string ResolvePath(IMetaInfo info); 19 | } 20 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.S3Storage/Cactus.Fileserver.S3Storage.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 4.0.0 6 | CactusSoft LLC 7 | Cactus.Fileserver.S3Storage 8 | Roman Barantsevich 9 | Amazone S3 storage for files 10 | Copyright CactusSoft 2017 11 | 12 | https://github.com/CactusSoft/Cactus.Fileserver 13 | true 14 | https://github.com/CactusSoft/Cactus.Fileserver 15 | MIT 16 | 4.0.4 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Cactus.Fileserver.S3Storage/ConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Amazon; 3 | using Amazon.Runtime; 4 | using Amazon.S3; 5 | using Cactus.Fileserver.Storage; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Cactus.Fileserver.S3Storage 10 | { 11 | public class S3FileserverBuilder 12 | { 13 | 14 | } 15 | public static class ConfigurationExtensions 16 | { 17 | public static IServiceCollection AddS3FileStorage(this IServiceCollection services, Action configureOptions) 18 | { 19 | services.Configure(configureOptions); 20 | 21 | services.AddScoped(c => 22 | { 23 | var config = c.GetRequiredService>().Value; 24 | return new AmazonS3Client(config.AccessKey, config.SecretKey, RegionEndpoint.GetBySystemName(config.Region)); 25 | }); 26 | 27 | services.AddSingleton(); 28 | services.AddScoped(); 29 | services.AddScoped(c => new S3FileStorage( 30 | c.GetRequiredService>().Value, 31 | c.GetRequiredService(), 32 | c.GetRequiredService(), 33 | c.GetRequiredService())); 34 | services.AddScoped(); 35 | 36 | return services; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Cactus.Fileserver.S3Storage/S3FileStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Mime; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Amazon.S3; 8 | using Amazon.S3.Model; 9 | using Cactus.Fileserver.Model; 10 | using Cactus.Fileserver.Storage; 11 | 12 | namespace Cactus.Fileserver.S3Storage 13 | { 14 | public class S3FileStorage : IFileStorage 15 | { 16 | protected readonly IS3FileStorageOptions Settings; 17 | protected readonly IAmazonS3 S3Client; 18 | protected readonly IStoredNameProvider NameProvider; 19 | protected readonly IUriResolver UriResolver; 20 | 21 | 22 | public S3FileStorage(IS3FileStorageOptions settings, IAmazonS3 s3Client, IStoredNameProvider nameProvider, IUriResolver uriResolver) 23 | { 24 | Settings = settings; 25 | S3Client = s3Client; 26 | NameProvider = nameProvider; 27 | UriResolver = uriResolver; 28 | } 29 | 30 | public virtual async Task Add(Stream stream, IMetaInfo info) 31 | { 32 | var streamToUpload = stream; 33 | var disposeRequired = false; 34 | if (!stream.CanSeek) 35 | { 36 | //AWS requires stream to have defined Length (be seakable) 37 | //Here the income stream could be 'raw from request' with unknown length 38 | //So, let's read it into MemoryStream as a buffer 39 | streamToUpload = new MemoryStream(); 40 | await stream.CopyToAsync(streamToUpload); 41 | streamToUpload.Seek(0, 0); 42 | disposeRequired = true; 43 | } 44 | 45 | try 46 | { 47 | var key = NameProvider.GetName(info); 48 | var putRequest = new PutObjectRequest 49 | { 50 | BucketName = Settings.BucketName, 51 | Key = key, 52 | InputStream = streamToUpload, 53 | AutoCloseStream = true, 54 | CannedACL = S3CannedACL.PublicRead 55 | }; 56 | var asciiOriginName = new string(Encoding.ASCII.GetChars(Encoding.ASCII.GetBytes(info.OriginalName)) 57 | .Select(c => c == '?' ? '-' : c) 58 | .ToArray()); 59 | putRequest.Metadata.Add(nameof(info.OriginalName), asciiOriginName); 60 | putRequest.Metadata.Add(nameof(info.MimeType), info.MimeType); 61 | putRequest.Metadata.Add(nameof(info.Owner), info.Owner); 62 | 63 | var res = await S3Client.PutObjectAsync(putRequest); 64 | info.InternalUri = new Uri($"https://s3.{Settings.Region}.amazonaws.com/{Settings.BucketName}/{key}"); 65 | info.Uri = UriResolver.ResolveUri(Settings.Region, Settings.BucketName, key); 66 | return info.Uri; 67 | } 68 | finally 69 | { 70 | if (disposeRequired) 71 | streamToUpload.Dispose(); 72 | } 73 | } 74 | 75 | public virtual Task Delete(IMetaInfo fileInfo) 76 | { 77 | var key = UriResolver.ResolveKey(fileInfo); 78 | var deleteRequest = new DeleteObjectRequest 79 | { 80 | BucketName = Settings.BucketName, 81 | Key = key 82 | }; 83 | return S3Client.DeleteObjectAsync(deleteRequest); 84 | } 85 | 86 | public virtual async Task Get(IMetaInfo fileInfo) 87 | { 88 | var filename = UriResolver.ResolveKey(fileInfo); 89 | var getRequest = new GetObjectRequest 90 | { 91 | BucketName = Settings.BucketName, 92 | Key = filename 93 | }; 94 | 95 | var response = await S3Client.GetObjectAsync(getRequest); 96 | return response.ResponseStream; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.S3Storage/S3FileStorageOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cactus.Fileserver.S3Storage 4 | { 5 | public interface IS3FileStorageOptions 6 | { 7 | string BucketName { get; } 8 | string Region { get; } 9 | } 10 | 11 | public interface IS3SecretOptions 12 | { 13 | string Region { get; } 14 | string AccessKey { get; } 15 | string SecretKey { get; } 16 | } 17 | 18 | public class S3FileStorageOptions : IS3FileStorageOptions, IS3SecretOptions 19 | { 20 | public Uri BaseUri { get; set; } 21 | public string BucketName { get; set; } 22 | public string Region { get; set; } 23 | public string AccessKey { get; set; } 24 | public string SecretKey { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.S3Storage/UriResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cactus.Fileserver.Model; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace Cactus.Fileserver.S3Storage 6 | { 7 | public interface IUriResolver 8 | { 9 | /// 10 | /// Returns URI to access the file 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | Uri ResolveUri(string region, string bucket, string key); 17 | 18 | /// 19 | /// Returns resource key based on requested url 20 | /// 21 | /// 22 | string ResolveKey(IMetaInfo fileInfo); 23 | } 24 | 25 | public class DirectS3UriResolver : IUriResolver 26 | { 27 | public const string AwsUrlFormat = "https://s3.{0}.amazonaws.com/{1}/{2}"; 28 | 29 | public Uri ResolveUri(string region, string bucket, string key) 30 | { 31 | return new Uri(string.Format(AwsUrlFormat, region, bucket, key)); 32 | } 33 | 34 | public string ResolveKey(IMetaInfo fileInfo) 35 | { 36 | return fileInfo.InternalUri.GetResource(); 37 | } 38 | } 39 | 40 | public class FileserverUriResolver : IUriResolver 41 | { 42 | private readonly string _baseUri; 43 | 44 | public FileserverUriResolver(IOptions options) 45 | { 46 | _baseUri = options.Value.BaseUri.ToString().TrimEnd('/'); 47 | } 48 | 49 | public Uri ResolveUri(string region, string bucket, string key) 50 | { 51 | return new Uri($"{_baseUri}/{bucket}/{key}"); 52 | } 53 | 54 | public string ResolveKey(IMetaInfo fileInfo) 55 | { 56 | return fileInfo.InternalUri.GetResource(); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Cactus.Fileserver.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | PreserveNewest 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Integration/CreateReadDeleteTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Cactus.Fileserver.Model; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | using Newtonsoft.Json; 11 | 12 | namespace Cactus.Fileserver.Tests.Integration 13 | { 14 | [TestClass] 15 | public class CreateReadDeleteTest : FileserverTestHost 16 | { 17 | [TestMethod] 18 | public async Task SingleTextCrudTest() 19 | { 20 | var content = "Hello world!"; 21 | var postRes = await Post(new StringContent(content)); 22 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 23 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 24 | var location = postRes.Headers.Location.ToString(); 25 | Assert.IsNotNull(location); 26 | 27 | var getRes = await Get(location); 28 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 29 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 30 | Assert.AreEqual(content.Length, (await getRes.Content.ReadAsByteArrayAsync()).Length); 31 | 32 | var delRes = await Delete(location); 33 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 34 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 35 | } 36 | 37 | [TestMethod] 38 | public async Task SingleImageCrudTest() 39 | { 40 | var file = "kartman.png"; 41 | var content = File.OpenRead(file); 42 | var httpContent = new StreamContent(content); 43 | httpContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); 44 | var postRes = await Post(httpContent); 45 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 46 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 47 | var location = postRes.Headers.Location.ToString(); 48 | Assert.IsNotNull(location); 49 | 50 | var getRes = await Get(location); 51 | if (getRes.StatusCode == HttpStatusCode.MovedPermanently) 52 | { 53 | getRes = await Get(getRes.Headers.Location.ToString()); 54 | } 55 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 56 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 57 | Assert.AreEqual(content.Length, (await getRes.Content.ReadAsByteArrayAsync()).Length); 58 | 59 | var delRes = await Delete(location); 60 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 61 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 62 | } 63 | 64 | [TestMethod] 65 | public async Task MultipartCrudTest() 66 | { 67 | var filename = "test.txt"; 68 | var fileContent = Encoding.ASCII.GetBytes("Hello world!"); 69 | var fileStream = new MemoryStream(fileContent); 70 | var postRes = await PostMultipart(fileStream, filename, "plain/text"); 71 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 72 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 73 | var location = postRes.Headers.Location.ToString(); 74 | Assert.IsNotNull(location); 75 | 76 | var getRes = await Get(location); 77 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 78 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 79 | Assert.AreEqual(fileContent.Length, (await getRes.Content.ReadAsByteArrayAsync()).Length); 80 | 81 | var delRes = await Delete(location); 82 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 83 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 84 | } 85 | 86 | [TestMethod] 87 | public async Task MultipleUploadTest() 88 | { 89 | var files = new[] 90 | { 91 | new FileUpload 92 | { 93 | FileName = "test1.txt", 94 | MimeType = "text/plain", 95 | Content = new MemoryStream(Encoding.ASCII.GetBytes("Hello world!")) 96 | }, 97 | new FileUpload 98 | { 99 | //FileName = "kartman.png", 100 | //Content = File.OpenRead("kartman.png"), 101 | //MimeType = "image/png" 102 | FileName = "test2.txt", 103 | MimeType = "text/plain", 104 | Content = new MemoryStream(Encoding.ASCII.GetBytes("second file content")) 105 | }, 106 | new FileUpload 107 | { 108 | FileName = "test3.txt", 109 | MimeType = "text/plain", 110 | Content = new MemoryStream(Encoding.ASCII.GetBytes("Hello world!!!!")) 111 | } 112 | }; 113 | 114 | var postRes = await PostMultipart(files); 115 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 116 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 117 | var location = postRes.Headers.Location.ToString(); 118 | Assert.IsNotNull(location); 119 | Assert.IsNotNull(postRes.Content); 120 | var metaData = JsonConvert.DeserializeObject(await postRes.Content.ReadAsStringAsync()); 121 | Assert.AreEqual(files[0].FileName, metaData[0].OriginalName); 122 | Assert.AreEqual(files[1].FileName, metaData[1].OriginalName); 123 | Assert.AreEqual(files[2].FileName, metaData[2].OriginalName); 124 | 125 | foreach (var meta in metaData) 126 | { 127 | var delRes = await Delete(meta.Uri.ToString()); 128 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 129 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 130 | } 131 | } 132 | 133 | [TestMethod] 134 | public async Task FileMetadataTest() 135 | { 136 | var filename = "test.txt"; 137 | var fileContent = Encoding.ASCII.GetBytes("Hello meta world!"); 138 | await using var fileStream = new MemoryStream(fileContent); 139 | var postRes = await PostMultipart(fileStream, filename, "plain/text"); 140 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 141 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 142 | var location = postRes.Headers.Location.ToString(); 143 | Assert.IsNotNull(location); 144 | 145 | var getRes = await Get(location + ".json"); 146 | Assert.IsTrue(getRes.IsSuccessStatusCode, $"Fail to get {location}.json: {getRes}"); 147 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 148 | Assert.IsNotNull(getRes.Content); 149 | var metaData = JsonConvert.DeserializeObject(await postRes.Content.ReadAsStringAsync()); 150 | Assert.AreEqual(filename, metaData.First().OriginalName); 151 | 152 | var delRes = await Delete(location); 153 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 154 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Integration/FileserverTestHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Threading.Tasks; 6 | using Cactus.Fileserver.Simple; 7 | using Microsoft.AspNetCore; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | 12 | namespace Cactus.Fileserver.Tests.Integration 13 | { 14 | [TestClass] 15 | public class FileserverTestHost 16 | { 17 | 18 | private static IWebHost _host; 19 | protected static string BaseUrl => "http://localhost:18047/"; 20 | //protected static TestServer _server; 21 | //protected static string BaseUrl => _server?.BaseAddress?.ToString(); 22 | 23 | [AssemblyInitialize] 24 | public static async Task StartSimpleFileserver(TestContext context) 25 | { 26 | Environment.SetEnvironmentVariable("ASPNETCORE_URLS", BaseUrl); 27 | //DO NOT USE: for some reason TestServer hangs if request is more then X bytes, so all image resizing tests fail 28 | //_server = new TestServer(new WebHostBuilder() 29 | // .UseStartup() 30 | // .UseConfiguration( 31 | // new ConfigurationBuilder() 32 | // .Build())); 33 | //_host = _server.Host; 34 | //await _host.StartAsync(); 35 | _host = WebHost.CreateDefaultBuilder() 36 | .ConfigureAppConfiguration((ctx, config) => 37 | { 38 | }) 39 | .ConfigureLogging((ctx, logging) => 40 | { 41 | }) 42 | .UseStartup() 43 | .Build(); 44 | await _host.StartAsync(); 45 | } 46 | 47 | [AssemblyCleanup] 48 | public static void Cleanup() 49 | { 50 | var stopped = _host?.StopAsync().Wait(TimeSpan.FromSeconds(5)); 51 | if (stopped ?? false) _host?.Dispose(); 52 | } 53 | 54 | protected Task Post(StringContent content) 55 | { 56 | var client = new HttpClient(); 57 | return client.PostAsync(BaseUrl, content); 58 | } 59 | 60 | protected Task Post(HttpContent content) 61 | { 62 | var client = new HttpClient(); 63 | return client.PostAsync(BaseUrl, content); 64 | } 65 | 66 | protected Task PostMultipart(string fullFilePath, string mimeType) 67 | { 68 | var content = File.OpenRead(fullFilePath); 69 | return PostMultipart(content, Path.GetFileName(fullFilePath), mimeType); 70 | } 71 | 72 | protected Task PostMultipart(Stream content, string fileName, string mimeType) 73 | { 74 | return PostMultipart(new FileUpload 75 | { 76 | Content = content, 77 | MimeType = mimeType, 78 | FileName = fileName 79 | }); 80 | } 81 | 82 | protected Task PostMultipart(params FileUpload[] upload) 83 | { 84 | var form = new MultipartFormDataContent(); 85 | foreach (var fileUpload in upload) 86 | { 87 | var fileContent = new StreamContent(fileUpload.Content); 88 | fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(fileUpload.MimeType); 89 | form.Add(fileContent, "file", fileUpload.FileName); 90 | } 91 | var client = new HttpClient(); 92 | return client.PostAsync(BaseUrl, form); 93 | } 94 | 95 | protected Task Get(string url) 96 | { 97 | var client = new HttpClient(new HttpClientHandler 98 | { 99 | AllowAutoRedirect = false 100 | }); 101 | return client.GetAsync(url); 102 | } 103 | 104 | protected Task Delete(string url) 105 | { 106 | var client = new HttpClient(); 107 | return client.DeleteAsync(url); 108 | } 109 | 110 | public class FileUpload 111 | { 112 | public Stream Content { get; set; } 113 | public string FileName { get; set; } 114 | public string MimeType { get; set; } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Integration/ImageResizeTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Headers; 7 | using System.Threading.Tasks; 8 | using Cactus.Fileserver.Aspnet.Dto; 9 | using Cactus.Fileserver.ImageResizer.Utils; 10 | using Cactus.Fileserver.Model; 11 | using Cactus.Fileserver.Simple; 12 | using Microsoft.AspNetCore; 13 | using Microsoft.AspNetCore.Hosting; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | using Newtonsoft.Json; 16 | 17 | namespace Cactus.Fileserver.Tests.Integration 18 | { 19 | [TestClass] 20 | public class ImageResizeTest: FileserverTestHost 21 | { 22 | [TestMethod] 23 | public async Task ImageOriginalStored() 24 | { 25 | var filename = "kartman.png"; 26 | var postRes = await PostMultipart(filename, "image/png"); 27 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 28 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 29 | Assert.IsNotNull(postRes.Headers.Location); 30 | 31 | var getRes = await Get(postRes.Headers.Location.ToString()); 32 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 33 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 34 | Assert.AreEqual((new FileInfo(filename)).Length, (await getRes.Content.ReadAsByteArrayAsync()).Length); 35 | Assert.AreEqual("image/png", getRes.Content.Headers.ContentType.MediaType); 36 | 37 | var delRes = await Delete(postRes.Headers.Location.ToString()); 38 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 39 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 40 | } 41 | 42 | [TestMethod] 43 | public async Task ImageDynamicallyResized() 44 | { 45 | var filename = "kartman.png"; 46 | var postRes = await PostMultipart(filename, "image/png"); 47 | 48 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 49 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 50 | Assert.IsNotNull(postRes.Headers.Location); 51 | var originFileLocation = postRes.Headers.Location.ToString(); 52 | 53 | var resizedUrl = originFileLocation + "?width=200&Height=200"; 54 | var getRes = await Get(resizedUrl); 55 | Assert.AreEqual(HttpStatusCode.MovedPermanently, getRes.StatusCode); 56 | Assert.IsNotNull(getRes.Headers.Location); 57 | var resizedFileLocation = getRes.Headers.Location.ToString(); 58 | 59 | getRes = await Get(resizedUrl); 60 | Assert.AreEqual(HttpStatusCode.MovedPermanently, getRes.StatusCode); 61 | Assert.AreEqual(resizedFileLocation, getRes.Headers.Location.ToString(), "No resizing should occurred second time, the reference should stay the same"); 62 | 63 | getRes = await Get(resizedFileLocation); 64 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 65 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 66 | Assert.IsTrue((new FileInfo(filename)).Length > (await getRes.Content.ReadAsByteArrayAsync()).Length); 67 | Assert.AreEqual("image/png", getRes.Content.Headers.ContentType.MediaType); 68 | 69 | var delRes = await Delete(resizedFileLocation); 70 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 71 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 72 | delRes = await Delete(originFileLocation); 73 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 74 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 75 | } 76 | 77 | [TestMethod] 78 | public async Task ImageDynamicallyResizedMetadataCheck() 79 | { 80 | var filename = "kartman.png"; 81 | var instructions200 = new ResizeInstructions { Width = 200, Height = 200 }; 82 | var postRes = await PostMultipart(filename, "image/png"); 83 | Assert.IsTrue(postRes.IsSuccessStatusCode, postRes.ToString()); 84 | Assert.AreEqual(HttpStatusCode.Created, postRes.StatusCode); 85 | Assert.IsNotNull(postRes.Headers.Location); 86 | Assert.IsNotNull(postRes.Content); 87 | var postResponse = JsonConvert.DeserializeObject(await postRes.Content.ReadAsStringAsync()); 88 | Assert.AreEqual(filename, postResponse.First().OriginalName); 89 | 90 | // Get and check 200x200 91 | var originLocation = postRes.Headers.Location.ToString(); 92 | var resizedUrl = originLocation + "?width=200&Height=200"; 93 | var getRes = await Get(resizedUrl); 94 | Assert.AreEqual(HttpStatusCode.MovedPermanently, getRes.StatusCode); 95 | Assert.IsNotNull(getRes.Headers.Location); 96 | var resizedFileLocation = getRes.Headers.Location.ToString(); 97 | 98 | getRes = await Get(resizedFileLocation); 99 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 100 | Assert.AreEqual(HttpStatusCode.OK, getRes.StatusCode, getRes.ToString()); 101 | Assert.IsTrue((new FileInfo(filename)).Length > (await getRes.Content.ReadAsByteArrayAsync()).Length); 102 | 103 | getRes = await Get(resizedFileLocation + ".json"); 104 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 105 | Assert.IsNotNull(getRes.Content); 106 | var meta = JsonConvert.DeserializeObject(await getRes.Content.ReadAsStringAsync()); 107 | Assert.AreEqual(originLocation, meta.Origin.ToString()); 108 | 109 | getRes = await Get(originLocation + ".json"); 110 | Assert.IsTrue(getRes.IsSuccessStatusCode, getRes.ToString()); 111 | Assert.IsNotNull(getRes.Content); 112 | meta = JsonConvert.DeserializeObject(await getRes.Content.ReadAsStringAsync()); 113 | Assert.IsNotNull(meta.Extra); 114 | Assert.IsNotNull(meta.Extra[instructions200.BuildSizeKey()]); 115 | 116 | var delRes = await Delete(originLocation); 117 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 118 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 119 | 120 | delRes = await Delete(resizedFileLocation); 121 | Assert.IsTrue(delRes.IsSuccessStatusCode, delRes.ToString()); 122 | Assert.AreEqual(HttpStatusCode.NoContent, delRes.StatusCode, delRes.ToString()); 123 | } 124 | 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Unit/ImageResizerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Specialized; 2 | using Cactus.Fileserver.ImageResizer; 3 | using Cactus.Fileserver.ImageResizer.Utils; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace Cactus.Fileserver.Tests.Unit 7 | { 8 | [TestClass] 9 | public class ImageResizerTest 10 | { 11 | [TestMethod] 12 | public void GetTargetSizeRatio05Test() 13 | { 14 | var res = ImageResizerService.GetTargetSize(new ResizeInstructions 15 | { 16 | Width = 100, 17 | Height = 100, 18 | KeepAspectRatio = true 19 | }, 0.5); 20 | Assert.AreEqual(50, res.Width); 21 | Assert.AreEqual(100, res.Height); 22 | } 23 | 24 | [TestMethod] 25 | public void GetTargetSizeRatio15Test() 26 | { 27 | var res = ImageResizerService.GetTargetSize(new ResizeInstructions 28 | { 29 | Width = 100, 30 | Height = 100, 31 | KeepAspectRatio = true 32 | }, 1.5); 33 | Assert.AreEqual(100, res.Width); 34 | Assert.AreEqual(67, res.Height); 35 | } 36 | 37 | [TestMethod] 38 | public void GetTargetSizeLimitedMaxTest() 39 | { 40 | var inst = new ResizeInstructions 41 | { 42 | MaxWidth = 100, 43 | MaxHeight = 100, 44 | Width = 10000, 45 | Height = 10000, 46 | KeepAspectRatio = true 47 | }; 48 | var res = ImageResizerService.GetTargetSize(inst, 1); 49 | Assert.AreEqual(inst.MaxWidth, res.Width); 50 | Assert.AreEqual(inst.MaxHeight, res.Height); 51 | } 52 | 53 | [TestMethod] 54 | public void GetTargetSizeLimitedMaxRatio05Test() 55 | { 56 | var inst = new ResizeInstructions 57 | { 58 | MaxWidth = 100, 59 | MaxHeight = 100, 60 | Width = 10000, 61 | Height = 10000, 62 | KeepAspectRatio = true 63 | }; 64 | var res = ImageResizerService.GetTargetSize(inst, 0.5); 65 | Assert.AreEqual(50, res.Width); 66 | Assert.AreEqual(100, res.Height); 67 | } 68 | 69 | [TestMethod] 70 | public void GetTargetSizeNotZeroTest() 71 | { 72 | var res = ImageResizerService.GetTargetSize(new ResizeInstructions 73 | { 74 | Width = 100, 75 | Height = null, 76 | KeepAspectRatio = true 77 | }, 1); 78 | Assert.AreEqual(100, res.Width); 79 | Assert.AreEqual(100, res.Height); 80 | } 81 | 82 | [TestMethod] 83 | public void BuildSizeKeyTest() 84 | { 85 | Assert.AreEqual("alt_size_800x600", new ResizeInstructions 86 | { 87 | Width = 800, 88 | Height = 600 89 | }.BuildSizeKey()); 90 | Assert.AreEqual("alt_size_800x-", new ResizeInstructions 91 | { 92 | Width = 800 93 | }.BuildSizeKey()); 94 | Assert.AreEqual("alt_size_-x-", new ResizeInstructions().BuildSizeKey()); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Unit/RandomNameProviderTest.cs: -------------------------------------------------------------------------------- 1 | using Cactus.Fileserver.Model; 2 | using Cactus.Fileserver.Storage; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Cactus.Fileserver.Tests.Unit 6 | { 7 | [TestClass] 8 | public class RandomNameProviderTest 9 | { 10 | [TestMethod] 11 | public void TestUnique() 12 | { 13 | var file = new MetaInfo(); 14 | var p = new RandomNameProvider(); 15 | var name1 = p.GetName(file); 16 | var name2 = p.GetName(file); 17 | var name3 = p.Regenerate(file, name2); 18 | 19 | Assert.IsFalse(string.IsNullOrEmpty(name1)); 20 | Assert.IsFalse(string.IsNullOrEmpty(name2)); 21 | Assert.IsFalse(string.IsNullOrEmpty(name3)); 22 | Assert.AreNotEqual(name1, name2); 23 | Assert.AreNotEqual(name1, name3); 24 | Assert.AreNotEqual(name2, name3); 25 | } 26 | 27 | [TestMethod] 28 | public void ExtensionStoredTest() 29 | { 30 | var p = new RandomNameProvider { StoreExt = true }; 31 | var name1 = p.GetName(new MetaInfo { OriginalName = "test.com" }); 32 | var name2 = p.GetName(new MetaInfo { OriginalName = "test." }); 33 | var name3 = p.GetName(new MetaInfo { OriginalName = "test" }); 34 | var name4 = p.GetName(new MetaInfo { OriginalName = "" }); 35 | var name5 = p.GetName(new MetaInfo { OriginalName = null }); 36 | 37 | 38 | Assert.IsTrue(name1.EndsWith(".com")); 39 | Assert.IsFalse(name2.EndsWith(".")); 40 | Assert.IsFalse(name2.Contains("test")); 41 | Assert.IsFalse(name3.EndsWith(".")); 42 | Assert.IsFalse(name3.Contains("test")); 43 | Assert.IsFalse(string.IsNullOrEmpty(name4)); 44 | Assert.IsFalse(string.IsNullOrEmpty(name5)); 45 | 46 | Assert.AreEqual(name4.Length, name5.Length); 47 | Assert.AreEqual(name3.Length, name4.Length); 48 | Assert.AreEqual(name2.Length, name3.Length); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Unit/ResizeInstructionTest.cs: -------------------------------------------------------------------------------- 1 | using Cactus.Fileserver.ImageResizer.Utils; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace Cactus.Fileserver.Tests.Unit 6 | { 7 | [TestClass] 8 | public class ResizeInstructionTest 9 | { 10 | [TestMethod] 11 | public void JoinDefaultTest() 12 | { 13 | var a = new ResizeInstructions 14 | { 15 | KeepAspectRatio = true, 16 | Width = 200, 17 | Height = 100 18 | }; 19 | var b = new ResizeInstructions 20 | { 21 | Width = 100, 22 | KeepAspectRatio = false 23 | }; 24 | b.Join(a); 25 | Assert.AreEqual(100, b.Width); 26 | Assert.AreEqual(100, b.Height); 27 | Assert.IsTrue(b.KeepAspectRatio.HasValue); 28 | Assert.IsFalse(b.KeepAspectRatio.Value); 29 | } 30 | 31 | [TestMethod] 32 | public void JoinManatoryTest() 33 | { 34 | var a = new ResizeInstructions 35 | { 36 | KeepAspectRatio = true, 37 | Width = 200, 38 | Height = 100, 39 | MaxWidth = 800, 40 | MaxHeight = 800 41 | }; 42 | var b = new ResizeInstructions 43 | { 44 | Width = 100, 45 | KeepAspectRatio = false, 46 | MaxWidth = 500 47 | }; 48 | b.Join(a, true); 49 | Assert.AreEqual(200, b.Width); 50 | Assert.AreEqual(100, b.Height); 51 | Assert.AreEqual(800, b.MaxWidth); 52 | Assert.AreEqual(800, b.MaxHeight); 53 | Assert.IsTrue(b.KeepAspectRatio.HasValue); 54 | Assert.IsTrue(b.KeepAspectRatio.Value); 55 | } 56 | 57 | [TestMethod] 58 | public void QuerySizeParseTest() 59 | { 60 | var q = new QueryString("?width=200&Height=300"); 61 | var res = new ResizeInstructions(q); 62 | 63 | Assert.AreEqual(200, res.Width); 64 | Assert.AreEqual(300, res.Height); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/Unit/UriExtensionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace Cactus.Fileserver.Tests.Unit 5 | { 6 | [TestClass] 7 | public class UriExtensionTest 8 | { 9 | [TestMethod] 10 | public void GetResourceTest() 11 | { 12 | Assert.AreEqual("file.ext", new Uri("http://srv.co/file.ext").GetResource()); 13 | Assert.AreEqual("file", new Uri("http://srv.co/file").GetResource()); 14 | Assert.AreEqual("file", new Uri("http://srv.co//file").GetResource()); 15 | Assert.AreEqual("file", new Uri("http://srv.co/folder/folder/file").GetResource()); 16 | Assert.AreEqual("file.ext", new Uri("http://srv.co/folder/folder/file.ext").GetResource()); 17 | Assert.AreEqual("file.ext", new Uri("http://srv.co//folder/folder/file.ext").GetResource()); 18 | Assert.AreEqual("", new Uri("http://srv.co").GetResource()); 19 | Assert.AreEqual("", new Uri("http://srv.co/folder/").GetResource()); 20 | Assert.AreEqual("80yoJH0M3GVAXxjm.jpg", new Uri("https://s3.eu-north-1.amazonaws.com/wrprod/80yoJH0M3GVAXxjm.jpg").GetResource()); 21 | 22 | } 23 | 24 | [TestMethod] 25 | public void GetFolderTest() 26 | { 27 | Assert.AreEqual(new Uri("http://srv.co/"), new Uri("http://srv.co/file.ext").GetFolder()); 28 | Assert.AreEqual(new Uri("http://srv.co/"), new Uri("http://srv.co//file.ext").GetFolder()); 29 | Assert.AreEqual(new Uri("http://srv.co/folder"), new Uri("http://srv.co/folder/file.ext").GetFolder()); 30 | Assert.AreEqual(new Uri("http://srv.co/folder1/folder2"), new Uri("http://srv.co/folder1/folder2/file.ext").GetFolder()); 31 | Assert.AreEqual(new Uri("http://srv.co//folder"), new Uri("http://srv.co//folder/file.ext").GetFolder()); 32 | Assert.AreEqual(new Uri("http://srv.co/folder/folder"), new Uri("http://srv.co/folder/folder/file.ext").GetFolder()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/kartman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Cactus.Fileserver.Tests/kartman.png -------------------------------------------------------------------------------- /Cactus.Fileserver.Tests/log4net.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Cactus.Fileserver.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30225.117 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver", "Cactus.Fileserver\Cactus.Fileserver.csproj", "{E4DEDC1B-6C96-4793-BD45-30B550DD1BB5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.LocalStorage", "Cactus.Fileserver.LocalStorage\Cactus.Fileserver.LocalStorage.csproj", "{4AE0236F-EDE1-4A4E-A3E5-1EF2C524AB46}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.AzureStorage", "Cactus.Fileserver.AzureStorage\Cactus.Fileserver.AzureStorage.csproj", "{B9AE74F7-36C9-4F1D-91BB-311443881AA5}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{41584938-146B-424F-8C10-374A64A01215}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.Simple", "Examples\Cactus.Fileserver.Simple\Cactus.Fileserver.Simple.csproj", "{63C09213-17DC-4095-BB5F-581CBA692C3E}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.S3Storage", "Cactus.Fileserver.S3Storage\Cactus.Fileserver.S3Storage.csproj", "{1E2E479C-22EE-4777-9C25-55BB2A8F22A4}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.ImageResizer", "Cactus.Fileserver.ImageResizer\Cactus.Fileserver.ImageResizer.csproj", "{CF1D7D06-FCE8-49E0-9D0C-9B58664018CE}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cactus.Fileserver.Tests", "Cactus.Fileserver.Tests\Cactus.Fileserver.Tests.csproj", "{B0FB8D6F-7D3E-48E7-88F5-76253A016981}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{32A207D8-134D-4BE0-A30B-10EFF5A514E1}" 23 | ProjectSection(SolutionItems) = preProject 24 | .travis.yml = .travis.yml 25 | EndProjectSection 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cactus.Fileserver.LocalStorage.Test", "Cactus.Fileserver.LocalStorage.Test\Cactus.Fileserver.LocalStorage.Test.csproj", "{2CF78959-669E-4245-B92C-CDF149758680}" 28 | EndProject 29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cactus.Fileserver.Aspnet", "Cactus.Fileserver.Aspnet\Cactus.Fileserver.Aspnet.csproj", "{5783F184-F7B0-420F-B990-77ED6A147A11}" 30 | EndProject 31 | Global 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {E4DEDC1B-6C96-4793-BD45-30B550DD1BB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {E4DEDC1B-6C96-4793-BD45-30B550DD1BB5}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {E4DEDC1B-6C96-4793-BD45-30B550DD1BB5}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {E4DEDC1B-6C96-4793-BD45-30B550DD1BB5}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {4AE0236F-EDE1-4A4E-A3E5-1EF2C524AB46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {4AE0236F-EDE1-4A4E-A3E5-1EF2C524AB46}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {4AE0236F-EDE1-4A4E-A3E5-1EF2C524AB46}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {4AE0236F-EDE1-4A4E-A3E5-1EF2C524AB46}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {B9AE74F7-36C9-4F1D-91BB-311443881AA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {B9AE74F7-36C9-4F1D-91BB-311443881AA5}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {B9AE74F7-36C9-4F1D-91BB-311443881AA5}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {B9AE74F7-36C9-4F1D-91BB-311443881AA5}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {63C09213-17DC-4095-BB5F-581CBA692C3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {63C09213-17DC-4095-BB5F-581CBA692C3E}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {63C09213-17DC-4095-BB5F-581CBA692C3E}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {63C09213-17DC-4095-BB5F-581CBA692C3E}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {1E2E479C-22EE-4777-9C25-55BB2A8F22A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {1E2E479C-22EE-4777-9C25-55BB2A8F22A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {1E2E479C-22EE-4777-9C25-55BB2A8F22A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {1E2E479C-22EE-4777-9C25-55BB2A8F22A4}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {CF1D7D06-FCE8-49E0-9D0C-9B58664018CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {CF1D7D06-FCE8-49E0-9D0C-9B58664018CE}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {CF1D7D06-FCE8-49E0-9D0C-9B58664018CE}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {CF1D7D06-FCE8-49E0-9D0C-9B58664018CE}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {B0FB8D6F-7D3E-48E7-88F5-76253A016981}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {B0FB8D6F-7D3E-48E7-88F5-76253A016981}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {B0FB8D6F-7D3E-48E7-88F5-76253A016981}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {B0FB8D6F-7D3E-48E7-88F5-76253A016981}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {2CF78959-669E-4245-B92C-CDF149758680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {2CF78959-669E-4245-B92C-CDF149758680}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {2CF78959-669E-4245-B92C-CDF149758680}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {2CF78959-669E-4245-B92C-CDF149758680}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {5783F184-F7B0-420F-B990-77ED6A147A11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {5783F184-F7B0-420F-B990-77ED6A147A11}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {5783F184-F7B0-420F-B990-77ED6A147A11}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {5783F184-F7B0-420F-B990-77ED6A147A11}.Release|Any CPU.Build.0 = Release|Any CPU 73 | EndGlobalSection 74 | GlobalSection(SolutionProperties) = preSolution 75 | HideSolutionNode = FALSE 76 | EndGlobalSection 77 | GlobalSection(NestedProjects) = preSolution 78 | {63C09213-17DC-4095-BB5F-581CBA692C3E} = {41584938-146B-424F-8C10-374A64A01215} 79 | EndGlobalSection 80 | GlobalSection(ExtensibilityGlobals) = postSolution 81 | SolutionGuid = {650FD418-DEA3-4EA9-92A6-DBF5B0004916} 82 | EndGlobalSection 83 | EndGlobal 84 | -------------------------------------------------------------------------------- /Cactus.Fileserver/Assembly.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Cactus.Fileserver.Tests")] 4 | -------------------------------------------------------------------------------- /Cactus.Fileserver/Cactus.Fileserver.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Cactus.Fileserver 6 | Cactus.Fileserver 7 | 4.0.4 8 | Lightweight fileserver 9 | Copyright CactusSoft 2016 10 | 11 | https://github.com/CactusSoft/Cactus.Fileserver 12 | true 13 | MIT 14 | 15 | 16 | 17 | bin\Release\ 18 | TRACE;LIBLOG_PUBLIC;LIBLOG_PORTABLE 19 | 20 | 21 | 22 | TRACE;DEBUG;LIBLOG_PUBLIC;LIBLOG_PORTABLE 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Cactus.Fileserver/FileStorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | using Cactus.Fileserver.Storage; 6 | 7 | namespace Cactus.Fileserver 8 | { 9 | public class FileStorageService : IFileStorageService 10 | { 11 | private readonly IFileStorage _fileStorage; 12 | private readonly IMetaInfoStorage _metaStorage; 13 | 14 | public FileStorageService(IMetaInfoStorage metaStorage, IFileStorage fileStorage) 15 | { 16 | _metaStorage = metaStorage; 17 | _fileStorage = fileStorage; 18 | } 19 | 20 | public async Task Get(Uri uri) 21 | { 22 | var info = await _metaStorage.Get(uri); 23 | return await _fileStorage.Get(info); 24 | } 25 | 26 | public async Task Create(Stream stream, IMetaInfo metaInfo) 27 | { 28 | metaInfo.Uri = await _fileStorage.Add(stream, metaInfo); 29 | await _metaStorage.Add(metaInfo); 30 | } 31 | 32 | public async Task Delete(Uri uri) 33 | { 34 | var info = await _metaStorage.Get(uri); 35 | await Task.WhenAll( 36 | _fileStorage.Delete(info), 37 | _metaStorage.Delete(uri) 38 | ); 39 | } 40 | 41 | public Task GetInfo(Uri uri) where T : IMetaInfo 42 | { 43 | return _metaStorage.Get(uri); 44 | } 45 | 46 | public Task UpdateInfo(IMetaInfo fileInfo) 47 | { 48 | return _metaStorage.Update(fileInfo); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/IFileStorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | 6 | namespace Cactus.Fileserver 7 | { 8 | public interface IFileStorageService 9 | { 10 | /// 11 | /// Get file content by URI 12 | /// 13 | /// 14 | /// 15 | Task Get(Uri uri); 16 | 17 | /// 18 | /// Store a new file from stream 19 | /// 20 | /// 21 | /// 22 | /// 23 | Task Create(Stream stream, IMetaInfo metaInfo); 24 | 25 | /// 26 | /// Delete file by URI 27 | /// 28 | /// 29 | /// 30 | Task Delete(Uri uri); 31 | 32 | /// 33 | /// Get file information 34 | /// 35 | /// 36 | /// 37 | Task GetInfo(Uri uri) where T : IMetaInfo; 38 | 39 | /// 40 | /// Update metadata 41 | /// 42 | /// Meta 43 | /// 44 | Task UpdateInfo(IMetaInfo fileInfo); 45 | 46 | } 47 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Model/IMetaInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cactus.Fileserver.Model 5 | { 6 | public interface IMetaInfo 7 | { 8 | /// 9 | /// URI to get the file 10 | /// 11 | Uri Uri { get; set; } 12 | 13 | /// 14 | /// Internal storage URI. Local file system path or S3/Azure storage URI 15 | /// 16 | Uri InternalUri { get; set; } 17 | 18 | /// 19 | /// URI of the origin. Used for files that are derivative from other, like resized image 20 | /// 21 | Uri Origin { get; set; } 22 | 23 | /// 24 | /// Mime type 25 | /// 26 | string MimeType { get; set; } 27 | 28 | /// 29 | /// Original name from user's system 30 | /// 31 | string OriginalName { get; set; } 32 | 33 | /// 34 | /// File owner 35 | /// 36 | string Owner { get; set; } 37 | 38 | /// 39 | /// URI of icon of the file 40 | /// 41 | Uri Icon { get; set; } 42 | 43 | /// 44 | /// Extra metadata 45 | /// 46 | IDictionary Extra { get; set; } 47 | } 48 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Model/MetaInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Cactus.Fileserver.Model 5 | { 6 | /// 7 | /// Represent file info. For example image that you uploaded pic.jpeg and received back URL like 8 | /// http://cdn.texas.srv.com/debug-folder/abcdf.png?x=y 9 | /// 10 | public class MetaInfo : IMetaInfo 11 | { 12 | public MetaInfo() 13 | { 14 | Extra = new Dictionary(); 15 | } 16 | 17 | public MetaInfo(IMetaInfo copyFrom):this() 18 | { 19 | if (copyFrom != null) 20 | { 21 | Uri = copyFrom.Uri; 22 | Origin = copyFrom.Origin; 23 | MimeType = copyFrom.MimeType; 24 | OriginalName = copyFrom.OriginalName; 25 | Owner = copyFrom.Owner; 26 | Icon = copyFrom.Icon; 27 | } 28 | } 29 | 30 | /// 31 | /// Full URL for getting the file. http://cdn.texas.srv.com/debug-folder/abcdf.png?x=y in our case 32 | /// 33 | public Uri Uri { get; set; } 34 | 35 | /// 36 | /// Internal storage URI. Local file system path or S3/Azure storage URI 37 | /// 38 | public Uri InternalUri { get; set; } 39 | 40 | /// 41 | /// Origin URI. 42 | /// 43 | public Uri Origin { get; set; } 44 | 45 | /// 46 | /// The stored file MIME type regarding RFC6838 47 | /// In our case "image/png" because the original file was converted into PNG format during uploading 48 | /// 49 | public string MimeType { get; set; } 50 | 51 | /// 52 | /// The original uploaded file name, "pic.jpg" in our case 53 | /// 54 | public string OriginalName { get; set; } 55 | 56 | /// 57 | /// File owner. Any string that will help you to define the file owner, e-mail for example 58 | /// 59 | public string Owner { get; set; } 60 | 61 | /// 62 | /// Icon or thumbnail URI. 63 | /// 64 | public Uri Icon { get; set; } 65 | 66 | /// 67 | /// Any extra parameters. The point for lightweight extensions. 68 | /// 69 | public IDictionary Extra { get; set; } 70 | } 71 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Storage/IFileStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Cactus.Fileserver.Model; 5 | 6 | namespace Cactus.Fileserver.Storage 7 | { 8 | /// 9 | /// Organize access to file based on URI 10 | /// 11 | public interface IFileStorage 12 | { 13 | //IUriResolver UriResolver { get; } 14 | 15 | /// 16 | /// Add file to the storage 17 | /// 18 | /// Stream 19 | /// File info. 20 | /// The meta information that could be used in some specific storage implementations like S3 or AzureBlob 21 | /// An URI that can be used to Get or Delete operations 22 | Task Add(Stream stream, IMetaInfo info); 23 | 24 | /// 25 | /// Delete file from storage 26 | /// 27 | /// 28 | Task Delete(IMetaInfo fileInfo); 29 | 30 | /// 31 | /// Get file content 32 | /// 33 | /// File content as a stream 34 | Task Get(IMetaInfo fileInfo); 35 | } 36 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Storage/IMetaInfoStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Cactus.Fileserver.Model; 4 | 5 | namespace Cactus.Fileserver.Storage 6 | { 7 | /// 8 | /// Storage of a file meta information. 9 | /// The meta information includes file its type, original name, owner and some other useful information. 10 | /// 11 | public interface IMetaInfoStorage 12 | { 13 | /// 14 | /// Put meta information to the storage 15 | /// 16 | /// Meta information. 17 | /// The Url field must be filled up - it uses to address the meta info in Get or Delete operations 18 | Task Add(T info) where T : IMetaInfo; 19 | 20 | /// 21 | /// Update meta information to the storage 22 | /// 23 | /// Meta information. 24 | /// The Url field must be filled up - it uses to address the meta info in Get or Delete operations 25 | Task Update(T info) where T : IMetaInfo; 26 | 27 | /// 28 | /// Delete meta information by the file URI 29 | /// 30 | /// A file URI. The same URI that was set in MetaInfo.Uri during Add operation 31 | Task Delete(Uri uri); 32 | 33 | /// 34 | /// Get a file metadata 35 | /// 36 | /// Metadata could be deserialized as any other type derived from MetaInfo 37 | /// A file URI. The same URI that was set in MetaInfo.Uri during Add operation 38 | /// Meta data 39 | Task Get(Uri uri) where T : IMetaInfo; 40 | } 41 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Storage/IStoredNameProvider.cs: -------------------------------------------------------------------------------- 1 | using Cactus.Fileserver.Model; 2 | 3 | namespace Cactus.Fileserver.Storage 4 | { 5 | public interface IStoredNameProvider 6 | { 7 | /// 8 | /// Returns generated anti name to store file in a storage. 9 | /// Its responsibility is to make it unique, randomly and short 10 | /// 11 | /// Meta file info 12 | /// Name 13 | string GetName(IMetaInfo info); 14 | 15 | /// 16 | /// Called if generated name was not unique and required to be regenerated. 17 | /// 18 | /// File info 19 | /// Previously generated result 20 | /// New name 21 | string Regenerate(IMetaInfo info, string duplicatedName); 22 | } 23 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/Storage/RandomNameProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Cactus.Fileserver.Model; 3 | 4 | namespace Cactus.Fileserver.Storage 5 | { 6 | public class RandomNameProvider : IStoredNameProvider 7 | { 8 | private readonly Random _randomNumberGenerator; 9 | private readonly byte[] _buffer; 10 | 11 | public RandomNameProvider() : this(12) 12 | { 13 | } 14 | 15 | public RandomNameProvider(int bytesCount) 16 | { 17 | _buffer = new byte[bytesCount]; 18 | _randomNumberGenerator = new Random(DateTime.UtcNow.Millisecond); 19 | StoreExt = true; 20 | } 21 | 22 | public bool StoreExt { get; set; } 23 | 24 | public string GetName(IMetaInfo info) 25 | { 26 | _randomNumberGenerator.NextBytes(_buffer); 27 | var res = Convert.ToBase64String(_buffer).Replace('+', '-').Replace('/', '_'); 28 | if (info.OriginalName != null && StoreExt) 29 | { 30 | var lastDot = info.OriginalName.LastIndexOf('.'); 31 | if (lastDot > 0 && lastDot < info.OriginalName.Length - 1) 32 | res += info.OriginalName.Substring(lastDot); 33 | } 34 | 35 | return res; 36 | } 37 | 38 | public string Regenerate(IMetaInfo info, string duplicatedName) 39 | { 40 | return GetName(info); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Cactus.Fileserver/UriExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | 5 | namespace Cactus.Fileserver 6 | { 7 | public static class UriExtension 8 | { 9 | public static readonly char UriPathSeparator = '/'; 10 | public static string GetResource(this Uri uri) 11 | { 12 | return Path.GetFileName(uri.AbsolutePath.TrimStart(UriPathSeparator)); 13 | } 14 | 15 | public static Uri GetFolder(this Uri uri) 16 | { 17 | if (uri == null) return null; 18 | if (uri.AbsolutePath.Length == 1 && uri.AbsolutePath[0] == UriPathSeparator) return uri; 19 | var str = uri.ToString(); 20 | return str.Last() == UriPathSeparator ? uri : new Uri(str.Substring(0, str.LastIndexOf(UriPathSeparator))); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/Cactus.Fileserver.Simple.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PreserveNewest 21 | 22 | 23 | PreserveNewest 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Cactus.Fileserver.Simple 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Cactus.Fileserver.Simple": { 4 | "commandName": "Project", 5 | "environmentVariables": { 6 | "ASPNETCORE_ENVIRONMENT": "Development" 7 | }, 8 | "applicationUrl": "http://localhost:18047/" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Cactus.Fileserver.Aspnet.Config; 5 | using Cactus.Fileserver.Aspnet.Middleware; 6 | using Cactus.Fileserver.ImageResizer; 7 | using Cactus.Fileserver.ImageResizer.Utils; 8 | using Cactus.Fileserver.LocalStorage.Config; 9 | using Cactus.Fileserver.S3Storage; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.StaticFiles; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using Microsoft.Extensions.FileProviders; 16 | using Microsoft.Extensions.FileProviders.Physical; 17 | using Microsoft.Extensions.Logging; 18 | 19 | namespace Cactus.Fileserver.Simple 20 | { 21 | public class Startup 22 | { 23 | private readonly IWebHostEnvironment _env; 24 | 25 | public Startup(IWebHostEnvironment env) 26 | { 27 | _env = env; 28 | } 29 | // This method gets called by the runtime. Use this method to add services to the container. 30 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 31 | public void ConfigureServices(IServiceCollection services) 32 | { 33 | services.AddLogging(); 34 | 35 | var url = Environment.GetEnvironmentVariable("ASPNETCORE_URLS"); 36 | if (string.IsNullOrEmpty(url)) 37 | { 38 | url = "http://localhost:18047"; 39 | } 40 | //url += url.EndsWith('/') ? "files/" : "/files/"; //<--- In case of using sub-path 41 | 42 | services.AddLocalFileStorage(o => 43 | { 44 | o.BaseFolder = _env.WebRootPath; 45 | o.BaseUri = new Uri(url); 46 | }); 47 | 48 | //services.AddS3FileStorage(o => 49 | //{ 50 | // o.BaseUri = new Uri(url); 51 | // o.BucketName = "cactussoft.fileserver.test"; 52 | // o.Region = "eu-central-1"; 53 | // o.AccessKey = "-"; 54 | // o.SecretKey = "-"; 55 | //}); 56 | 57 | services.AddLocalMetaStorage(o => 58 | { 59 | o.BaseFolder = _env.WebRootPath; 60 | }); 61 | 62 | services.AddDynamicResizing(o => 63 | { 64 | o.MandatoryInstructions = new ResizeInstructions { MaxHeight = 4068, MaxWidth = 4068 }; 65 | o.DefaultInstructions = new ResizeInstructions { KeepAspectRatio = true }; 66 | }); 67 | } 68 | 69 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 70 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) 71 | { 72 | loggerFactory.AddLog4Net(); 73 | var storageFolder = new DirectoryInfo(env.WebRootPath); 74 | if (!storageFolder.Exists) 75 | storageFolder.Create(); 76 | 77 | app 78 | .UseDeveloperExceptionPage() 79 | //.Map("/files", branch => branch //<--- In case of using sub-path 80 | .UseDynamicResizing() 81 | .UseGetFile(b => b 82 | //.UseMiddleware()) //Redirect to S3/Azure for case of public accessible bucket 83 | //.UseMiddleware()) //Slower, but full-controlled 84 | .UseStaticFiles(new StaticFileOptions //Files will be retrieved using StaticFiles middleware which is fast but insecure 85 | { 86 | FileProvider = new PhysicalFileProvider(storageFolder.FullName, ExclusionFilters.None), 87 | DefaultContentType = "application/octet-stream", 88 | ServeUnknownFileTypes = true, //<---- do not use on production to prevent upload executable content like viruses 89 | ContentTypeProvider = new FileExtensionContentTypeProvider(new Dictionary 90 | { 91 | { ".json", "application/json"}, 92 | { ".svg", "image/svg+xml"}, 93 | { ".png", "image/png"} 94 | }) 95 | })) 96 | .UseAddFile() 97 | .UseDelFile() 98 | 99 | //Default handler for requests that are not targeted to the file server 100 | .Run(async context => 101 | { 102 | if (context.Request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase)) 103 | { 104 | context.Response.StatusCode = 404; 105 | await context.Response.WriteAsync("There's nothing here, my little friend."); 106 | } 107 | else 108 | { 109 | // Strange request. 110 | context.Response.StatusCode = 400; 111 | await context.Response.WriteAsync("You do something wrong. What are you waiting for, a Christmas mystery?"); 112 | } 113 | }); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/log4net.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047//files/17GfJps2PCX06jSd.txt","Origin":null,"StoragePath":"//files/17GfJps2PCX06jSd.txt","MimeType":"text/plain","OriginalName":"credits.txt","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/17GfJps2PCX06jSd.txt: -------------------------------------------------------------------------------- 1 | Find the online version of this document here: https://fiddler2.com/r/?credits 2 | 3 | Fiddler depends on the work of a number of other companies and individuals: 4 | 5 | Some of Fiddler's icons were generously provided by the FatCow.com free icon set, licensed under Creative Commons. http://www.fatcow.com/free-icons 6 | Fiddler is installed using the excellent freeware NSIS Installer. http://nsis.sourceforge.net/Main_Page 7 | Fiddler's HexView inspector uses an early fork of what is now the HexEditor control http://sourceforge.net/projects/hexbox/ Copyright (c) 2011 Bernhard Elbl - Licensed under the MIT License. http://www.opensource.org/licenses/mit-license.php 8 | Fiddler's JSON parsing support is based on a sample Copyright (c)2008 Patrick van Bergen - Licensed under the MIT License. 9 | Fiddler's JavaScript formatting code is based on a sample �2012 Jonathan Wood - Licensed under the Microsoft Public License. http://www.microsoft.com/opensource/licenses.mspx 10 | Fiddler uses the commercial Xceed Libraries for compressing SAZ files and compressing/decompressing web traffic. http://xceed.com/ 11 | The Quantum Whale Editor.NET component is used for the SyntaxView Inspector and FiddlerScript Editors. http://www.qwhale.net/products/editor.htm 12 | 13 | dwebp.exe is Copyright (c)2010, Google Inc. and supplied under the following license: 14 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 15 | -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 16 | -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 17 | -Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 20 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | zopfli.exe is Copyright (c)2011, Google Inc. and supplied under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/3tenEHtSwZFfqXpx.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/3tenEHtSwZFfqXpx.txt -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/4sRpySbooGfEQXFW.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/4sRpySbooGfEQXFW.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/4sRpySbooGfEQXFW.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/4sRpySbooGfEQXFW.jpg","Origin":null,"StoragePath":"/4sRpySbooGfEQXFW.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/59kc8cD7lyITeniC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/59kc8cD7lyITeniC.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/59kc8cD7lyITeniC.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/59kc8cD7lyITeniC.jpg","Origin":null,"StoragePath":"/59kc8cD7lyITeniC.jpg","MimeType":"image/jpeg","OriginalName":"002.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/623U8TvsnTwOKYMU.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/623U8TvsnTwOKYMU.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/623U8TvsnTwOKYMU.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/623U8TvsnTwOKYMU.jpg","Origin":null,"StoragePath":"/623U8TvsnTwOKYMU.jpg","MimeType":"image/jpeg","OriginalName":"002.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/NonEGsJqfdv7PZCd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/NonEGsJqfdv7PZCd.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/NonEGsJqfdv7PZCd.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/NonEGsJqfdv7PZCd.jpg","Origin":null,"StoragePath":"/NonEGsJqfdv7PZCd.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/RRmhkED4tndt0X1B.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/RRmhkED4tndt0X1B.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/RRmhkED4tndt0X1B.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/RRmhkED4tndt0X1B.jpg","Origin":null,"StoragePath":"/RRmhkED4tndt0X1B.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{"alt-size-260x260":"http://localhost:18047/juRt0OK0tWprDnBH.jpg"}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/Wqf0k8WL10Ht3JAc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/Wqf0k8WL10Ht3JAc.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/Wqf0k8WL10Ht3JAc.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/Wqf0k8WL10Ht3JAc.jpg","Origin":null,"StoragePath":"/Wqf0k8WL10Ht3JAc.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/X8Q6qShBQglPl_w8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/X8Q6qShBQglPl_w8.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/X8Q6qShBQglPl_w8.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/X8Q6qShBQglPl_w8.jpg","Origin":null,"StoragePath":"/X8Q6qShBQglPl_w8.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{"alt-size-100x100":"http://localhost:18047/dN7ucueKCfKmDb9D.jpg","alt-size-NAx500":"http://localhost:18047/njKrLZONPbU871tS.jpg"}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/_CgVvhMoija-Q-IV.txt: -------------------------------------------------------------------------------- 1 | Find the online version of this document here: https://fiddler2.com/r/?credits 2 | 3 | Fiddler depends on the work of a number of other companies and individuals: 4 | 5 | Some of Fiddler's icons were generously provided by the FatCow.com free icon set, licensed under Creative Commons. http://www.fatcow.com/free-icons 6 | Fiddler is installed using the excellent freeware NSIS Installer. http://nsis.sourceforge.net/Main_Page 7 | Fiddler's HexView inspector uses an early fork of what is now the HexEditor control http://sourceforge.net/projects/hexbox/ Copyright (c) 2011 Bernhard Elbl - Licensed under the MIT License. http://www.opensource.org/licenses/mit-license.php 8 | Fiddler's JSON parsing support is based on a sample Copyright (c)2008 Patrick van Bergen - Licensed under the MIT License. 9 | Fiddler's JavaScript formatting code is based on a sample �2012 Jonathan Wood - Licensed under the Microsoft Public License. http://www.microsoft.com/opensource/licenses.mspx 10 | Fiddler uses the commercial Xceed Libraries for compressing SAZ files and compressing/decompressing web traffic. http://xceed.com/ 11 | The Quantum Whale Editor.NET component is used for the SyntaxView Inspector and FiddlerScript Editors. http://www.qwhale.net/products/editor.htm 12 | 13 | dwebp.exe is Copyright (c)2010, Google Inc. and supplied under the following license: 14 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 15 | -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 16 | -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 17 | -Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 20 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | zopfli.exe is Copyright (c)2011, Google Inc. and supplied under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/bYqzr8vZ7V8xm53I.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/bYqzr8vZ7V8xm53I.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/bYqzr8vZ7V8xm53I.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/bYqzr8vZ7V8xm53I.jpg","Origin":null,"StoragePath":"/bYqzr8vZ7V8xm53I.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{"alt-size-200x200":"http://localhost:18047/Wqf0k8WL10Ht3JAc.jpg","alt-size-300x300":"http://localhost:18047/zGQbOo2yH6L98skc.jpg","alt-size-250x250":"http://localhost:18047/RRmhkED4tndt0X1B.jpg"}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/dN7ucueKCfKmDb9D.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/dN7ucueKCfKmDb9D.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/dN7ucueKCfKmDb9D.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/dN7ucueKCfKmDb9D.jpg","Origin":null,"StoragePath":"/dN7ucueKCfKmDb9D.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{"alt-size-600x600":"http://localhost:18047/NonEGsJqfdv7PZCd.jpg"}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/eSyNpOSpJV55F8-v.txt: -------------------------------------------------------------------------------- 1 | Find the online version of this document here: https://fiddler2.com/r/?credits 2 | 3 | Fiddler depends on the work of a number of other companies and individuals: 4 | 5 | Some of Fiddler's icons were generously provided by the FatCow.com free icon set, licensed under Creative Commons. http://www.fatcow.com/free-icons 6 | Fiddler is installed using the excellent freeware NSIS Installer. http://nsis.sourceforge.net/Main_Page 7 | Fiddler's HexView inspector uses an early fork of what is now the HexEditor control http://sourceforge.net/projects/hexbox/ Copyright (c) 2011 Bernhard Elbl - Licensed under the MIT License. http://www.opensource.org/licenses/mit-license.php 8 | Fiddler's JSON parsing support is based on a sample Copyright (c)2008 Patrick van Bergen - Licensed under the MIT License. 9 | Fiddler's JavaScript formatting code is based on a sample �2012 Jonathan Wood - Licensed under the Microsoft Public License. http://www.microsoft.com/opensource/licenses.mspx 10 | Fiddler uses the commercial Xceed Libraries for compressing SAZ files and compressing/decompressing web traffic. http://xceed.com/ 11 | The Quantum Whale Editor.NET component is used for the SyntaxView Inspector and FiddlerScript Editors. http://www.qwhale.net/products/editor.htm 12 | 13 | dwebp.exe is Copyright (c)2010, Google Inc. and supplied under the following license: 14 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 15 | -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 16 | -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 17 | -Neither the name of Google nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 20 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 22 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | 24 | zopfli.exe is Copyright (c)2011, Google Inc. and supplied under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/fXINhOS0ku4DuR9U: -------------------------------------------------------------------------------- 1 | MY COOL FILE CONTENT IS HERE!!!!! -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/fXINhOS0ku4DuR9U.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/fXINhOS0ku4DuR9U","Origin":null,"StoragePath":"/fXINhOS0ku4DuR9U","MimeType":"text/plain","OriginalName":"1tag","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/i78fV6TsnMbaN6r8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/i78fV6TsnMbaN6r8.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/i78fV6TsnMbaN6r8.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/i78fV6TsnMbaN6r8.jpg","Origin":null,"StoragePath":"/i78fV6TsnMbaN6r8.jpg","MimeType":"image/jpeg","OriginalName":"002.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/juRt0OK0tWprDnBH.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/juRt0OK0tWprDnBH.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/juRt0OK0tWprDnBH.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/juRt0OK0tWprDnBH.jpg","Origin":null,"StoragePath":"/juRt0OK0tWprDnBH.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/njKrLZONPbU871tS.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/njKrLZONPbU871tS.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/njKrLZONPbU871tS.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/njKrLZONPbU871tS.jpg","Origin":null,"StoragePath":"/njKrLZONPbU871tS.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{"alt-size-NAx800":"http://localhost:18047/4sRpySbooGfEQXFW.jpg"}} -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/nothing.txt: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/zGQbOo2yH6L98skc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CactusSoft/Cactus.Fileserver/da5b17a3bb4e01e163bdce1b0a11d22ce242b60f/Examples/Cactus.Fileserver.Simple/wwwroot/zGQbOo2yH6L98skc.jpg -------------------------------------------------------------------------------- /Examples/Cactus.Fileserver.Simple/wwwroot/zGQbOo2yH6L98skc.jpg.json: -------------------------------------------------------------------------------- 1 | {"Uri":"http://localhost:18047/zGQbOo2yH6L98skc.jpg","Origin":null,"StoragePath":"/zGQbOo2yH6L98skc.jpg","MimeType":"image/jpeg","OriginalName":"027.jpg","Owner":null,"Icon":null,"Extra":{}} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 CactusSoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cactus.Fileserver 2 | [ ![Download](https://travis-ci.com/CactusSoft/Cactus.Fileserver.svg?branch=develop) ](https://travis-ci.com/CactusSoft/Cactus.Fileserver) 3 | [ ![Download](https://codecov.io/gh/CactusSoft/Cactus.Fileserver/graph/badge.svg) ](https://codecov.io/gh/CactusSoft/Cactus.Fileserver) 4 | 5 | Simple lib for fast building your own file storage microservice. Implemented as OWIN handler for Katana as welll as Asp.NetCore. 6 | 7 | Look at LocalFileserverExample for details. 8 | 9 | Features: 10 | - HTTP POST (multipart & not) to upload a file 11 | - HTTP DELETE to drop a file 12 | - Supporting S3 & Azure BLOB storages. Returns direct URL to stored file. 13 | - Flexible piplene for new adding files 14 | - Supports local filesystem storage for simple projects & dev environment 15 | --------------------------------------------------------------------------------