Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use pre-gzipped content files? #1584

Closed
AnderssonPeter opened this issue Jun 21, 2016 · 23 comments
Closed

Use pre-gzipped content files? #1584

AnderssonPeter opened this issue Jun 21, 2016 · 23 comments

Comments

@AnderssonPeter
Copy link

AnderssonPeter commented Jun 21, 2016

is there any support for pre-gzipped files if there where we could compress css, html and javascript files with zopfli to make then as small as possible.

the files could be saved as
[filename]_[hash].gzip or some like that.

If there is support how would one go about to enable it?
If there isn't do you have any tips on how to create it?

@neyromant
Copy link

neyromant commented Jun 21, 2016

I use gulp-gzip package for make .gz files.
And small middleware for handle requests for static files:

public class CompressedStaticFileMiddleware
    {
        private IHostingEnvironment _hostingEnv;
        private StaticFileMiddleware _base;
        public CompressedStaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory) 
        {
            _hostingEnv = hostingEnv;
            var contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
_base = new StaticFileMiddleware(next, hostingEnv, new StaticFileOptions()
            {
                ContentTypeProvider = contentTypeProvider,
                FileProvider = options.Value.FileProvider ?? hostingEnv.WebRootFileProvider,
                OnPrepareResponse = ctx =>
                {
                    const string ext = ".gz";
                    if (ctx.File.Name.EndsWith(ext))
                    {
                        string contentType = null;
                        if(contentTypeProvider.TryGetContentType(ctx.File.PhysicalPath.Remove(ctx.File.PhysicalPath.Length - ext.Length, ext.Length), out contentType))
                            ctx.Context.Response.ContentType = contentType;
                        ctx.Context.Response.Headers.Add("Content-Encoding", new[] { "gzip" });
                    }
                }
            }, loggerFactory);
        }

        public Task Invoke(HttpContext context)
        {
            if(context.Request.Path.HasValue)
            {
                string acceptEncoding = context.Request.Headers["Accept-Encoding"];
                if (
                    !string.IsNullOrEmpty(acceptEncoding) && 
                    (acceptEncoding.Contains("gzip") && 
                    System.IO.File.Exists(_hostingEnv.MapPath(context.Request.Path.Value.StartsWith("/") ? context.Request.Path.Value.Remove(0, 1) : context.Request.Path.Value) + ".gz"))
                )
                {
                    context.Request.Path = new PathString(context.Request.Path.Value + ".gz");
                    return _base.Invoke(context);
                }
            }
            return _base.Invoke(context);
        }
    }

@AnderssonPeter
Copy link
Author

AnderssonPeter commented Jul 4, 2016

the constructor for StaticFileMiddleware takes a IOptions<StaticFileOptions> not a StaticFileOptions so i cant compile this?
And IHostingEnvironment does not seem to contain MapPath?

@neyromant
Copy link

neyromant commented Jul 8, 2016

@AnderssonPeter, yes, it was code for RC1.
For RC2 , you can try this:

public class CompressedStaticFileMiddleware
    {
        private IHostingEnvironment _hostingEnv;
        private StaticFileMiddleware _base;
        public CompressedStaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
        {
            _hostingEnv = hostingEnv;
            var contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
            options.Value.ContentTypeProvider = contentTypeProvider;
            options.Value.FileProvider = options.Value.FileProvider ?? hostingEnv.WebRootFileProvider;
            options.Value.OnPrepareResponse = ctx =>
            {
                const string ext = ".gz";
                if (ctx.File.Name.EndsWith(ext))
                {
                    string contentType = null;
                    if (contentTypeProvider.TryGetContentType(ctx.File.PhysicalPath.Remove(ctx.File.PhysicalPath.Length - ext.Length, ext.Length), out contentType))
                        ctx.Context.Response.ContentType = contentType;
                    ctx.Context.Response.Headers.Add("Content-Encoding", new[] { "gzip" });
                }
            };

            _base = new StaticFileMiddleware(next, hostingEnv, options, loggerFactory);
        }

        public Task Invoke(HttpContext context)
        {
            if (context.Request.Path.HasValue)
            {
                string acceptEncoding = context.Request.Headers["Accept-Encoding"];
                if (
                    !string.IsNullOrEmpty(acceptEncoding) &&
                    (
                        acceptEncoding.Contains("gzip") &&
                        System.IO.File.Exists(
                            System.IO.Path.Combine(
                                _hostingEnv.WebRootPath, context.Request.Path.Value.StartsWith("/") 
                                ? context.Request.Path.Value.Remove(0, 1) 
                                : context.Request.Path.Value
                            ) + ".gz"
                        )
                    )
                )
                {
                    context.Request.Path = new PathString(context.Request.Path.Value + ".gz");
                    return _base.Invoke(context);
                }
            }
            return _base.Invoke(context);
        }
    }

@AnderssonPeter
Copy link
Author

Ill try it when i get home, but im using RTM not RC2 ill get back to you if this works in the RTM.

Thanks @neyromant !!

@neyromant
Copy link

You are welcome.
I think that in the RTM will not have problems, because these classes have not changed.

@AnderssonPeter
Copy link
Author

Im confused does asp.net core gzip content on the fly?
If i don`t add the "CompressedStaticFileMiddleware" i still get GZipped content, and it does not matter if the .gz files are there or not so i guess they aren't used.

Also it seems like the Invoke method is only called once so it never has a chance to replace the file paths with .gz

@neyromant
Copy link

I found small mistake in code:

System.IO.File.Exists(
System.IO.Path.Combine(
_hostingEnv.WebRootPath, context.Request.Path.Value.StartsWith("/")
? context.Request.Path.Value.Remove(0, 1)
: context.Request.Path.Value
) + ".gz"
)

@AnderssonPeter
Copy link
Author

Well some of my confusion has been lifted, the default project from visual studio runs IIS Express as a reverse proxy infront of Kestrel and IIS Express does automatic GZip compression on content.

So that was the reason for files being gzipped even when there was no .gz file.

Ill try your updated code when i get home and see if it works better!

Thanks @neyromant

@neyromant
Copy link

@AnderssonPeter, You are welcome.

@AnderssonPeter
Copy link
Author

AnderssonPeter commented Jul 13, 2016

I made some improvements to support brotli compression, and if the browser supports both it picks the smallest file.

Now i haven't been able to test brotli for some reason its only used on https (Chrome and FireFox) and im to new to asp.net core and Kestrel to configure local https!

public class CompressedStaticFileMiddleware
{
    private static Dictionary<string, string> compressionTypes = new Dictionary<string, string>()
    {{"gzip", ".gz" },
        {"br", ".br" }};

    private IHostingEnvironment _hostingEnv;
    private StaticFileMiddleware _base;
    public CompressedStaticFileMiddleware(RequestDelegate next, IHostingEnvironment hostingEnv, IOptions<StaticFileOptions> options, ILoggerFactory loggerFactory)
    {
        _hostingEnv = hostingEnv;
        var contentTypeProvider = options.Value.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
        options.Value.ContentTypeProvider = contentTypeProvider;
        options.Value.FileProvider = options.Value.FileProvider ?? hostingEnv.WebRootFileProvider;
        options.Value.OnPrepareResponse = ctx =>
        {
            foreach(var compressionType in compressionTypes.Keys)
            {
                var fileExtension = compressionTypes[compressionType];
                if (ctx.File.Name.EndsWith(fileExtension, StringComparison.OrdinalIgnoreCase))
                {
                    string contentType = null;
                    if (contentTypeProvider.TryGetContentType(ctx.File.PhysicalPath.Remove(ctx.File.PhysicalPath.Length - fileExtension.Length, fileExtension.Length), out contentType))
                        ctx.Context.Response.ContentType = contentType;
                    ctx.Context.Response.Headers.Add("Content-Encoding", new[] { compressionType });
                }
            }
        };

        _base = new StaticFileMiddleware(next, hostingEnv, options, loggerFactory);
    }

    public Task Invoke(HttpContext context)
    {
        if (context.Request.Path.HasValue)
        {
            string acceptEncoding = context.Request.Headers["Accept-Encoding"];
            FileInfo matchedFile = null;
            string[] browserSupportedCompressionTypes = context.Request.Headers["Accept-Encoding"].ToString().Split(new[] {',', ' '}, StringSplitOptions.RemoveEmptyEntries);
            foreach (var compressionType in compressionTypes.Keys)
            {
                if (browserSupportedCompressionTypes.Contains(compressionType, StringComparer.OrdinalIgnoreCase))
                {
                    var fileExtension = compressionTypes[compressionType];
                    var filePath = System.IO.Path.Combine(
                            _hostingEnv.WebRootPath, context.Request.Path.Value.StartsWith("/")
                            ? context.Request.Path.Value.Remove(0, 1)
                            : context.Request.Path.Value
                        ) + fileExtension;
                    var file = new FileInfo(filePath);
                    if (file.Exists)
                    {
                        if (matchedFile == null)
                            matchedFile = file;
                        else if (matchedFile.Length > file.Length)
                            matchedFile = file;
                    }
                }
            }
            if (matchedFile != null)
            {
                context.Request.Path = new PathString(context.Request.Path.Value + matchedFile.Extension);
                return _base.Invoke(context);
            }
        }
        return _base.Invoke(context);
    }
}

gulp-brotli can be used to compress the files with brotli it seems to beat gzip even when gzipped with zopfli.

@AnderssonPeter
Copy link
Author

@neyromant is it ok if i create a github page with this code? and if any license you would prefer?
And if i figure out how ill create a nuget package.

@neyromant
Copy link

@AnderssonPeter, Of course, you can create a page and nuget package.
I will be happy if you are somewhere in the comments write my nickname :)

@AnderssonPeter
Copy link
Author

Github page is up for it: https://github.com/AnderssonPeter/CompressedStaticFiles haven't managed to add nuget yet tho.

@AnderssonPeter
Copy link
Author

And its up on nuget https://www.nuget.org/packages/CompressedStaticFiles/

@Ponant
Copy link
Contributor

Ponant commented Aug 19, 2016

@AnderssonPeter
I reported a seemingly small issue on Microsoft Azure Forum as we have a production website that does not send compressed svg files. A moderator there pointed to this thread and I am trying to implement your package but I fail to make it work.

  • I installed your package on our AP.NET Core RTM 1.0.0
  • added app.UseCompressedStaticFiles(); before app.UseStaticFiles();
  • Removing .UseIISIntegration() in Program.cs makes the website unreachable both on localhost and on azure, so I left it in...
  • Uploading to Azure and checking out in the browser that the SVG files of 6kB and 13kB are not compressed.

Any clue?
Thanks

@AnderssonPeter
Copy link
Author

i don't know any thing about Azure but i guess it uses IIS?
if that's true then im sorry the package wont work it only works when you use Kestrel.

@Ponant
Copy link
Contributor

Ponant commented Aug 19, 2016

I am using IIS indeed, thanks for the reply, I also realized this won't work.

@PeterDefendo
Copy link

@Ponant i merged AnderssonPeter/CompressedStaticFiles#4 sometime ago so it might work now but im not 100% sure.

@Ponant
Copy link
Contributor

Ponant commented Sep 4, 2016

Thanks @PeterDefendo , but where are the instructions that I try out your merge? Is it here-->
https://github.com/AnderssonPeter/CompressedStaticFiles ?

@AnderssonPeter
Copy link
Author

@Ponant sorry i happend to write with my work github account :P sorry about the confusion, its in the official nuget package

@ilya-git
Copy link

ilya-git commented Nov 13, 2016

Thanks for the nice plugin, I have advertised it a bit here :) But I do really hope MS includes in standard package in this issue. Maybe you can help them by making a pull request :)

@aspnet-hello
Copy link

This issue is being closed because it has not been updated in 3 months.

We apologize if this causes any inconvenience. We ask that if you are still encountering this issue, please log a new issue with updated information and we will investigate.

@tkrotoff
Copy link

@aspnet-hello fuck stupid bots

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants