lib/roda/plugins/timestamp_public.rb



# frozen-string-literal: true

#
class Roda
  module RodaPlugins
    # The timestamp_public plugin adds a +timestamp_path+ method for constructing
    # timestamp paths, and a +r.timestamp_public+ routing method to serve static files
    # from a directory (using the public plugin).  This plugin is useful when you want
    # to modify the path to static files when the modify timestamp on the file changes,
    # ensuring that requests for the static file will not be cached.
    #
    # Note that while this plugin will not serve files outside of the public directory,
    # for performance reasons it does not check the path of the file is inside the public
    # directory when getting the modify timestamp.  If the +timestamp_path+ method is
    # called with untrusted input, it is possible for an attacker to get the modify
    # timestamp for any file on the file system.
    #
    # Examples:
    #
    #   # Use public folder as location of files, and static as the path prefix
    #   plugin :timestamp_public
    #
    #   # Use /path/to/app/static as location of files, and public as the path prefix
    #   opts[:root] = '/path/to/app'
    #   plugin :public, root: 'static', prefix: 'public'
    #
    #   # Assuming public is the location of files, and static is the path prefix
    #   route do
    #     # Make GET /static/1238099123/images/foo.png look for public/images/foo.png 
    #     r.timestamp_public
    #
    #     r.get "example" do
    #       # "/static/1238099123/images/foo.png"
    #       timestamp_path("images/foo.png")
    #     end
    #   end
    module TimestampPublic
      # Use options given to setup timestamped file serving.  The following option is
      # recognized by the plugin:
      #
      # :prefix :: The prefix for paths, before the timestamp segment
      #
      # The options given are also passed to the public plugin.
      def self.configure(app, opts={})
        app.plugin :public, opts
        app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.opts[:timestamp_public_prefix] || "static").dup.freeze
      end

      module InstanceMethods
        # Return a path to the static file that could be served by r.timestamp_public.
        # This does not check the file is inside the directory for performance reasons,
        # so this should not be called with untrusted input.
        def timestamp_path(file)
          mtime = File.mtime(File.join(opts[:public_root], file))
          "/#{opts[:timestamp_public_prefix]}/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/#{file}"
        end
      end

      module RequestMethods
        # Serve files from the public directory if the file exists,
        # it includes the timestamp_public prefix segment followed by
        # a integer segment for the timestamp, and this is a GET request.
        def timestamp_public
          if is_get?
            on roda_class.opts[:timestamp_public_prefix], Integer do |_|
              public
            end
          end
        end
      end
    end

    register_plugin(:timestamp_public, TimestampPublic)
  end
end