├── .gitattributes ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Runfile ├── lib ├── webcache.rb └── webcache │ ├── cache_operations.rb │ ├── response.rb │ ├── version.rb │ └── web_cache.rb ├── spec ├── spec_helper.rb └── webcache │ ├── response_spec.rb │ ├── web_cache_eigenclass_spec.rb │ └── web_cache_spec.rb └── webcache.gemspec /.gitattributes: -------------------------------------------------------------------------------- 1 | Runfile linguist-language=Ruby 2 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: { branches: master } 5 | 6 | jobs: 7 | test: 8 | name: Ruby ${{ matrix.ruby }} 9 | 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: { ruby: ['3.0', '3.1', '3.2', '3.3'] } 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Install OS dependencies 20 | run: sudo apt-get -y install libyaml-dev 21 | 22 | - name: Setup Ruby 23 | uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: '${{ matrix.ruby }}' 26 | bundler-cache: true 27 | 28 | # this step is added since the public httpbin.prg is sometimes overloaded 29 | - name: Start the httpbin test server 30 | run: docker run -it --rm -d -p 3000:80 --name httpbin kennethreitz/httpbin 31 | 32 | - name: Run tests 33 | run: bundle exec rspec 34 | env: 35 | HTTPBIN_HOST: http://localhost:3000 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | /.yardoc 3 | /cache 4 | /coverage 5 | /debug.runfile 6 | /dev 7 | /doc 8 | /Gemfile.lock 9 | /gems -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | --color 3 | --format documentation 4 | --fail-fast -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-rspec 3 | - rubocop-performance 4 | 5 | inherit_gem: 6 | rentacop: 7 | - rentacop.yml 8 | - rspec.yml 9 | 10 | AllCops: 11 | TargetRubyVersion: 3.0 12 | Exclude: 13 | - 'debug.rb' 14 | - 'dev/**/*' 15 | 16 | # Allow a longer module 17 | Metrics/ModuleLength: 18 | Max: 120 19 | 20 | # Allow `Marshal.load`, since we want to get the Ruby object from cache 21 | # We assume trusted source for all caching operations 22 | Security/MarshalLoad: 23 | Enabled: false 24 | 25 | # Allow non standard spec file name 26 | RSpec/FilePath: 27 | Enabled: false 28 | RSpec/SpecFilePathFormat: 29 | Enabled: false 30 | 31 | # Alerts by this cop are irrelevant 32 | RSpec/Rails: 33 | Enabled: false 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Change Log 2 | ======================================== 3 | 4 | v0.9.0 - 2023-05-19 5 | ---------------------------------------- 6 | 7 | - Drop support for Ruby < 2.6 8 | - Add ability to set cache file permissions 9 | - Drop support for Ruby 2.x 10 | 11 | 12 | v0.8.0 - 2021-05-24 13 | ---------------------------------------- 14 | 15 | - Update http gem and drop support for ruby < 2.5.0 16 | 17 | 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'byebug' 4 | gem 'lp' 5 | gem 'rspec' 6 | gem 'runfile', require: false 7 | gem 'runfile-tasks', require: false 8 | gem 'simplecov' 9 | 10 | gemspec 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Danny Ben Shitrit 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebCache 2 | 3 | [![Gem Version](https://badge.fury.io/rb/webcache.svg)](https://badge.fury.io/rb/webcache) 4 | [![Build Status](https://github.com/DannyBen/webcache/workflows/Test/badge.svg)](https://github.com/DannyBen/webcache/actions?query=workflow%3ATest) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/022f555211d47d655988/maintainability)](https://codeclimate.com/github/DannyBen/webcache/maintainability) 6 | 7 | --- 8 | 9 | Hassle-free caching for HTTP download. 10 | 11 | --- 12 | 13 | ## Install 14 | 15 | ``` 16 | $ gem install webcache 17 | ``` 18 | 19 | Or with bundler: 20 | 21 | ```ruby 22 | gem 'webcache' 23 | ``` 24 | 25 | ## Usage 26 | 27 | WebCache can be used both as an instance, and as a static class. 28 | 29 | ```ruby 30 | require 'webcache' 31 | 32 | # Instance 33 | cache = WebCache.new life: '3h' 34 | response = cache.get 'http://example.com' 35 | 36 | # Static 37 | WebCache.life = '3h' 38 | WebCache.get 'http://example.com' 39 | ``` 40 | 41 | The design intention is to provide both a globally available singleton 42 | `WebCache` object, as well as multiple caching instances, with different 43 | settings - depending on the use case. 44 | 45 | Note that the examples in this README are all using the instance syntax, but 46 | all methods are also available statically. 47 | 48 | This is the basic usage pattern: 49 | 50 | ```ruby 51 | require 'webcache' 52 | cache = WebCache.new 53 | response = cache.get 'http://example.com' 54 | puts response # => "..." 55 | puts response.content # => same as above 56 | puts response.to_s # => same as above 57 | puts response.error # => nil 58 | puts response.base_uri # => "http://example.com/" 59 | ``` 60 | 61 | By default, the cached objects are stored in the `./cache` directory, and 62 | expire after 60 minutes. The cache directory will be created as needed, and 63 | the permissions of the cached files can be specified if needed. 64 | 65 | You can change these settings on initialization: 66 | 67 | ```ruby 68 | cache = WebCache.new dir: 'tmp/my_cache', life: '3d', permissions: 0o640 69 | response = cache.get 'http://example.com' 70 | ``` 71 | 72 | Or later: 73 | 74 | ```ruby 75 | cache = WebCache.new 76 | cache.dir = 'tmp/my_cache' 77 | cache.life = '4h' 78 | cache.permissions = 0o640 79 | response = cache.get 'http://example.com' 80 | ``` 81 | 82 | The `life` property accepts any of these formats: 83 | 84 | ```ruby 85 | cache.life = 10 # 10 seconds 86 | cache.life = '20s' # 20 seconds 87 | cache.life = '10m' # 10 minutes 88 | cache.life = '10h' # 10 hours 89 | cache.life = '10d' # 10 days 90 | ``` 91 | 92 | Use the `cached?` method to check if a URL is cached: 93 | 94 | ```ruby 95 | cache = WebCache.new 96 | cache.cached? 'http://example.com' 97 | # => false 98 | 99 | response = cache.get 'http://example.com' 100 | cache.cached? 'http://example.com' 101 | # => true 102 | ``` 103 | 104 | Use `enable` and `disable` to toggle caching on and off: 105 | 106 | ```ruby 107 | cache = WebCache.new 108 | cache.disable 109 | cache.enabled? 110 | # => false 111 | 112 | response = cache.get 'http://example.com' 113 | cache.cached? 'http://example.com' 114 | # => false 115 | 116 | cache.enable 117 | response = cache.get 'http://example.com' 118 | cache.cached? 'http://example.com' 119 | # => true 120 | ``` 121 | 122 | Use `clear url` to remove a cached object if it exists: 123 | 124 | ```ruby 125 | cache = WebCache.new 126 | response = cache.get 'http://example.com' 127 | cache.cached? 'http://example.com' 128 | # => true 129 | 130 | cache.clear 'http://example.com' 131 | cache.cached? 'http://example.com' 132 | # => false 133 | ``` 134 | 135 | Use `flush` to delete the entire cache directory: 136 | 137 | ```ruby 138 | cache = WebCache.new 139 | cache.flush 140 | ``` 141 | 142 | Use `force: true` to force download even if the object is cached: 143 | 144 | ```ruby 145 | cache = WebCache.new 146 | response = cache.get 'http://example.com', force: true 147 | ``` 148 | 149 | ## Authentication 150 | 151 | To configure an authentication header, use the `auth` option. Similarly to 152 | the other options, this can be set directly on the static class, on instance 153 | initialization, or later on the instance: 154 | 155 | ```ruby 156 | cache = WebCache.new auth: '...' 157 | cache.get 'http://example.com' # authenticated 158 | 159 | cache = WebCache.new 160 | cache.auth = '...' 161 | cache.get 'http://example.com' # authenticated 162 | 163 | WebCache.auth = '...' 164 | WebCache.get 'http://example.com' # authenticated 165 | ``` 166 | 167 | For basic authentication, provide a hash: 168 | 169 | ```ruby 170 | cache = WebCache.new auth: { user: 'user', pass: 's3cr3t' } 171 | ``` 172 | 173 | For other authentication headers, simply provide the header string: 174 | 175 | ```ruby 176 | cache = WebCache.new auth: "Bearer t0k3n" 177 | ``` 178 | 179 | ## Response Object 180 | 181 | The response object holds these properties: 182 | 183 | ### `response.content` 184 | 185 | Contains the HTML content. In case of an error, this will include the 186 | error message. The `#to_s` method of the response object also returns 187 | the same content. 188 | 189 | ### `response.error` 190 | 191 | In case of an error, this contains the error message, `nil` otherwise. 192 | 193 | ### `response.code` 194 | 195 | Contains the HTTP code, or `nil` if there was a non-HTTP error. 196 | 197 | ### `response.success?` 198 | 199 | A convenience method, returns true if `error` is empty. 200 | 201 | ### `response.base_uri` 202 | 203 | Contains the actual address of the page. This is useful when the request 204 | is redirected. For example, `http://example.com` will set the 205 | `base_uri` to `http://example.com/` (note the trailing slash). 206 | 207 | ## Related Projects 208 | 209 | For a similar gem that provides general purpose caching, see the 210 | [Lightly gem][lightly]. 211 | 212 | ## Contributing / Support 213 | 214 | If you experience any issue, have a question or a suggestion, or if you wish 215 | to contribute, feel free to [open an issue][issues]. 216 | 217 | --- 218 | 219 | [lightly]: https://github.com/DannyBen/lightly 220 | [issues]: https://github.com/DannyBen/webcache/issues 221 | -------------------------------------------------------------------------------- /Runfile: -------------------------------------------------------------------------------- 1 | require 'webcache' 2 | 3 | title "WebCache Developer Toolbelt" 4 | summary "Runfile tasks for building the WebCache gem" 5 | version WebCache::VERSION 6 | 7 | import_gem 'runfile-tasks/gem' 8 | import 'debug' 9 | -------------------------------------------------------------------------------- /lib/webcache.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | require 'webcache/cache_operations' 4 | require 'webcache/response' 5 | require 'webcache/web_cache' 6 | 7 | if ENV['BYEBUG'] 8 | require 'byebug' 9 | require 'lp' 10 | end 11 | -------------------------------------------------------------------------------- /lib/webcache/cache_operations.rb: -------------------------------------------------------------------------------- 1 | require 'digest/md5' 2 | require 'fileutils' 3 | require 'http' 4 | 5 | class WebCache 6 | module CacheOperations 7 | attr_accessor :permissions 8 | attr_reader :last_error, :user, :pass, :auth 9 | attr_writer :dir 10 | 11 | def initialize(dir: 'cache', life: '1h', auth: nil, permissions: nil) 12 | @dir = dir 13 | @life = life_to_seconds life 14 | @enabled = true 15 | @auth = convert_auth auth 16 | @permissions = permissions 17 | end 18 | 19 | def get(url, force: false) 20 | return http_get url unless enabled? 21 | 22 | path = get_path url 23 | clear url if force || stale?(path) 24 | 25 | get! path, url 26 | end 27 | 28 | def life 29 | @life ||= 3600 30 | end 31 | 32 | def life=(new_life) 33 | @life = life_to_seconds new_life 34 | end 35 | 36 | def dir 37 | @dir ||= 'cache' 38 | end 39 | 40 | def cached?(url) 41 | path = get_path url 42 | File.exist?(path) and !stale?(path) 43 | end 44 | 45 | def enabled? 46 | @enabled ||= (@enabled.nil? ? true : @enabled) 47 | end 48 | 49 | def enable 50 | @enabled = true 51 | end 52 | 53 | def disable 54 | @enabled = false 55 | end 56 | 57 | def clear(url) 58 | path = get_path url 59 | FileUtils.rm path if File.exist? path 60 | end 61 | 62 | def flush 63 | FileUtils.rm_rf dir if Dir.exist? dir 64 | end 65 | 66 | def auth=(auth) 67 | convert_auth auth 68 | end 69 | 70 | private 71 | 72 | def get!(path, url) 73 | return load_file_content path if File.exist? path 74 | 75 | response = http_get url 76 | save_file_content path, response unless !response || response.error 77 | response 78 | end 79 | 80 | def get_path(url) 81 | File.join dir, Digest::MD5.hexdigest(url) 82 | end 83 | 84 | def load_file_content(path) 85 | Marshal.load File.binread(path) 86 | end 87 | 88 | def save_file_content(path, response) 89 | FileUtils.mkdir_p dir 90 | File.open path, 'wb', permissions do |file| 91 | file.write Marshal.dump(response) 92 | end 93 | end 94 | 95 | def http_get(url) 96 | Response.new http_response(url) 97 | rescue => e 98 | url = URI.parse url 99 | Response.new error: e.message, base_uri: url, content: e.message 100 | end 101 | 102 | def basic_auth? 103 | !!(user and pass) 104 | end 105 | 106 | def http_response(url) 107 | if basic_auth? 108 | HTTP.basic_auth(user: user, pass: pass).follow.get url 109 | elsif auth 110 | HTTP.auth(auth).follow.get url 111 | else 112 | HTTP.follow.get url 113 | end 114 | end 115 | 116 | def stale?(path) 117 | life.positive? and File.exist?(path) and Time.new - File.mtime(path) >= life 118 | end 119 | 120 | def life_to_seconds(arg) 121 | arg = arg.to_s 122 | 123 | case arg[-1] 124 | when 's' then arg[0..].to_i 125 | when 'm' then arg[0..].to_i * 60 126 | when 'h' then arg[0..].to_i * 60 * 60 127 | when 'd' then arg[0..].to_i * 60 * 60 * 24 128 | else; arg.to_i 129 | end 130 | end 131 | 132 | def convert_auth(opts) 133 | @user = nil 134 | @pass = nil 135 | @auth = nil 136 | 137 | if opts.respond_to?(:has_key?) && opts.has_key?(:user) && opts.has_key?(:pass) 138 | @user = opts[:user] 139 | @pass = opts[:pass] 140 | else 141 | @auth = opts 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/webcache/response.rb: -------------------------------------------------------------------------------- 1 | class WebCache 2 | class Response 3 | attr_accessor :error, :base_uri, :content, :code 4 | 5 | def initialize(opts = {}) 6 | case opts 7 | when HTTP::Response then init_with_http_response opts 8 | when Hash then init_with_hash opts 9 | end 10 | end 11 | 12 | def to_s 13 | content 14 | end 15 | 16 | def success? 17 | !error 18 | end 19 | 20 | private 21 | 22 | def init_with_http_response(response) 23 | @base_uri = response.uri 24 | @code = response.code 25 | if response.status.success? 26 | @content = response.to_s 27 | @error = nil 28 | else 29 | @content = response.status.to_s 30 | @error = response.status.to_s 31 | end 32 | end 33 | 34 | def init_with_hash(opts) 35 | @error = opts[:error] 36 | @base_uri = opts[:base_uri] 37 | @content = opts[:content] 38 | @code = opts[:code] 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/webcache/version.rb: -------------------------------------------------------------------------------- 1 | class WebCache 2 | VERSION = '0.9.0' 3 | end 4 | -------------------------------------------------------------------------------- /lib/webcache/web_cache.rb: -------------------------------------------------------------------------------- 1 | class WebCache 2 | include CacheOperations 3 | 4 | class << self 5 | include CacheOperations 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'rubygems' 5 | require 'bundler' 6 | Bundler.require :default, :development 7 | 8 | def httpbin_host 9 | ENV['HTTPBIN_HOST'] || 'https://httpbin.org' 10 | end 11 | -------------------------------------------------------------------------------- /spec/webcache/response_spec.rb: -------------------------------------------------------------------------------- 1 | describe WebCache::Response do 2 | describe '#new' do 3 | context 'with hash' do 4 | let :response do 5 | described_class.new({ 6 | error: 'problemz', 7 | base_uri: 'sky.net', 8 | content: 'robots', 9 | code: 200, 10 | }) 11 | end 12 | 13 | it 'sets error' do 14 | expect(response.error).to eq 'problemz' 15 | end 16 | 17 | it 'sets base_uri' do 18 | expect(response.base_uri).to eq 'sky.net' 19 | end 20 | 21 | it 'sets content' do 22 | expect(response.content).to eq 'robots' 23 | end 24 | 25 | it 'sets code' do 26 | expect(response.code).to eq 200 27 | end 28 | end 29 | 30 | context 'with HTTP response' do 31 | let(:http_response) { HTTP.follow.get 'http://example.com' } 32 | let(:response) { described_class.new http_response } 33 | 34 | it 'sets error' do 35 | expect(response.error).to be_nil 36 | end 37 | 38 | it 'sets base_uri' do 39 | expect(response.base_uri.to_s).to eq 'http://example.com/' 40 | end 41 | 42 | it 'sets content' do 43 | expect(response.content).to match 'Example Domain' 44 | end 45 | 46 | it 'sets code' do 47 | expect(response.code).to eq 200 48 | end 49 | end 50 | end 51 | 52 | describe '#to_s' do 53 | let(:response) { described_class.new content: 'robots' } 54 | 55 | it 'returns the content' do 56 | expect(response.to_s).to eq 'robots' 57 | end 58 | end 59 | 60 | describe '#success?' do 61 | context 'when there was an error' do 62 | let(:response) { described_class.new error: 'robots' } 63 | 64 | it 'returns false' do 65 | expect(response.success?).to be false 66 | end 67 | end 68 | 69 | context 'when there was no error' do 70 | let(:response) { described_class.new content: 'robots' } 71 | 72 | it 'returns true' do 73 | expect(response.success?).to be true 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/webcache/web_cache_eigenclass_spec.rb: -------------------------------------------------------------------------------- 1 | describe WebCache do 2 | subject { described_class } 3 | 4 | it 'has good defaults' do 5 | expect(subject.dir).to eq 'cache' 6 | expect(subject.life).to eq 3600 7 | end 8 | 9 | it 'is enabled by default' do 10 | expect(subject).to be_enabled 11 | end 12 | 13 | it 'behaves as a WebCache instance' do 14 | instance_methods = subject.new.methods - Object.methods 15 | class_methods = subject.methods - Object.methods 16 | 17 | expect(instance_methods).to eq class_methods 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/webcache/web_cache_spec.rb: -------------------------------------------------------------------------------- 1 | describe WebCache do 2 | let(:url) { 'http://example.com' } 3 | 4 | before { subject.flush } 5 | 6 | describe '#new' do 7 | it 'sets default properties' do 8 | expect(subject.life).to eq 3600 9 | expect(subject.dir).to eq 'cache' 10 | end 11 | 12 | context 'with arguments' do 13 | subject { described_class.new dir: 'store', life: 120, auth: auth } 14 | 15 | let(:auth) { { user: 'user', pass: 's3cr3t' } } 16 | 17 | it 'sets its properties' do 18 | expect(subject.life).to eq 120 19 | expect(subject.dir).to eq 'store' 20 | expect(subject.user).to eq auth[:user] 21 | expect(subject.pass).to eq auth[:pass] 22 | end 23 | end 24 | end 25 | 26 | describe '#get' do 27 | it 'saves a file' do 28 | subject.get url 29 | expect(Dir['cache/*']).not_to be_empty 30 | end 31 | 32 | it 'downloads from the web' do 33 | expect(subject).to receive(:http_get).with(url) 34 | subject.get url 35 | end 36 | 37 | it 'loads from cache' do 38 | subject.get url 39 | expect(subject).to be_cached url 40 | expect(subject).not_to receive(:http_get).with(url) 41 | expect(subject).to receive(:load_file_content) 42 | subject.get url 43 | end 44 | 45 | it 'returns content from cache' do 46 | subject.get url 47 | expect(subject).to be_cached url 48 | response = subject.get url 49 | expect(response.content.length).to be > 500 50 | end 51 | 52 | context 'with file permissions' do 53 | before do 54 | subject.permissions = 0o600 55 | FileUtils.rm_f tmp_path 56 | end 57 | 58 | let(:tmp_path) { '/tmp/webcache-test-file' } 59 | let(:file_mode) { File.stat(tmp_path).mode & 0o777 } 60 | 61 | it 'chmods the cache file after saving' do 62 | allow(subject).to receive(:get_path).with(url).and_return tmp_path 63 | subject.get url 64 | expect(file_mode).to eq 0o600 65 | end 66 | end 67 | 68 | context 'with force: true' do 69 | it 'always downloads a fresh copy' do 70 | subject.get url 71 | expect(subject).to be_cached url 72 | expect(subject).to receive(:http_get).with(url) 73 | subject.get url, force: true 74 | end 75 | end 76 | 77 | context 'when cache is disabled' do 78 | before { subject.disable } 79 | 80 | it 'skips caching' do 81 | subject.get url 82 | expect(Dir['cache/*']).to be_empty 83 | end 84 | end 85 | 86 | context 'when cache dir does not exist' do 87 | before { expect(Dir).not_to exist 'cache' } 88 | 89 | it 'creates it' do 90 | subject.get url 91 | expect(Dir).to exist 'cache' 92 | end 93 | end 94 | 95 | context 'when the request is successful' do 96 | let(:response) { subject.get url } 97 | 98 | it 'sets response content' do 99 | expect(response.content).to match 'Example Domain' 100 | end 101 | 102 | it 'sets response code' do 103 | expect(response.code).to eq 200 104 | end 105 | 106 | it 'sets response base_uri' do 107 | expect(response.base_uri).to be_a HTTP::URI 108 | expect(response.base_uri.to_s).to eq 'http://example.com/' 109 | end 110 | 111 | it 'sets error to nil' do 112 | expect(response.error).to be_nil 113 | end 114 | end 115 | 116 | context 'with 404 url' do 117 | let(:response) { subject.get 'http://example.com/not_found' } 118 | 119 | it 'returns the error message' do 120 | expect(response.content).to eq '404 Not Found' 121 | end 122 | 123 | it 'sets error to the error message' do 124 | expect(response.error).to eq '404 Not Found' 125 | end 126 | 127 | it 'sets code to 404' do 128 | expect(response.code).to eq 404 129 | end 130 | end 131 | 132 | context 'with a bad url' do 133 | let(:response) { subject.get 'http://not-a-uri' } 134 | 135 | it 'returns the error message' do 136 | expect(response.content).to match 'failed to connect' 137 | end 138 | 139 | it 'sets error to the error message' do 140 | expect(response.error).to match 'failed to connect' 141 | end 142 | end 143 | 144 | context 'with https' do 145 | let(:response) { subject.get 'https://en.wikipedia.org/wiki/HTTPS' } 146 | 147 | before { subject.disable } 148 | 149 | it 'downloads from the web' do 150 | expect(response.content.size).to be > 40_000 151 | expect(response.error).to be_nil 152 | end 153 | end 154 | 155 | context 'with basic authentication' do 156 | let(:response) { subject.get "#{httpbin_host}/basic-auth/user/pass" } 157 | 158 | context 'when the credentials are valid' do 159 | before { subject.auth = { user: 'user', pass: 'pass' } } 160 | 161 | it 'downloads from the web' do 162 | expect(response).to be_success 163 | content = JSON.parse response.content 164 | expect(content['authenticated']).to be true 165 | end 166 | end 167 | 168 | context 'when the credentials are invalid' do 169 | before { subject.auth = { user: 'user', pass: 'wrong-pass' } } 170 | 171 | it 'fails' do 172 | expect(response).not_to be_success 173 | expect(response.code).to eq 401 174 | end 175 | end 176 | end 177 | 178 | context 'with other authentication header' do 179 | let(:response) { subject.get "#{httpbin_host}/bearer" } 180 | 181 | before { subject.auth = 'Bearer t0k3n' } 182 | 183 | it 'downloads from the web' do 184 | expect(response).to be_success 185 | content = JSON.parse response.content 186 | expect(content['authenticated']).to be true 187 | expect(content['token']).to eq 't0k3n' 188 | end 189 | end 190 | end 191 | 192 | describe '#cached?' do 193 | it 'returns true when url is cached' do 194 | subject.get url 195 | expect(subject).to be_cached url 196 | end 197 | 198 | it 'returns false when url is not cached' do 199 | expect(subject).not_to be_cached 'http://never.downloaded.com' 200 | end 201 | end 202 | 203 | describe '#enable' do 204 | it 'enables http calls' do 205 | subject.enable 206 | expect(subject).to be_enabled 207 | expect(subject).to receive(:http_get) 208 | subject.get url 209 | end 210 | end 211 | 212 | describe '#disable' do 213 | it 'disables cache handling' do 214 | subject.disable 215 | expect(subject).not_to be_enabled 216 | expect(subject).to receive(:http_get).twice 217 | expect(subject).not_to receive(:load_file_content) 218 | subject.get url 219 | subject.get url 220 | end 221 | end 222 | 223 | describe '#clear' do 224 | before do 225 | subject.get url 226 | expect(Dir).not_to be_empty subject.dir 227 | end 228 | 229 | it 'removes a url cache file' do 230 | subject.clear url 231 | expect(Dir).to be_empty subject.dir 232 | end 233 | end 234 | 235 | describe '#flush' do 236 | before do 237 | subject.get url 238 | expect(Dir).not_to be_empty subject.dir 239 | end 240 | 241 | it 'deletes the entire cache directory' do 242 | subject.flush 243 | expect(Dir).not_to exist subject.dir 244 | end 245 | end 246 | 247 | describe '#life=' do 248 | it 'handles plain numbers' do 249 | subject.life = 11 250 | expect(subject.life).to eq 11 251 | end 252 | 253 | it 'handles 11s as seconds' do 254 | subject.life = '11s' 255 | expect(subject.life).to eq 11 256 | end 257 | 258 | it 'handles 11m as minutes' do 259 | subject.life = '11m' 260 | expect(subject.life).to eq 11 * 60 261 | end 262 | 263 | it 'handles 11h as hours' do 264 | subject.life = '11h' 265 | expect(subject.life).to eq 11 * 60 * 60 266 | end 267 | 268 | it 'handles 11d as days' do 269 | subject.life = '11d' 270 | expect(subject.life).to eq 11 * 60 * 60 * 24 271 | end 272 | end 273 | 274 | describe '#permissions=' do 275 | it 'sets file permissions' do 276 | subject.permissions = 0o600 277 | expect(subject.permissions).to eq 0o600 278 | end 279 | end 280 | end 281 | -------------------------------------------------------------------------------- /webcache.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('lib', __dir__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'webcache/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'webcache' 7 | s.version = WebCache::VERSION 8 | s.summary = 'Hassle-free caching for HTTP download' 9 | s.description = 'Easy to use file cache for web downloads' 10 | s.authors = ['Danny Ben Shitrit'] 11 | s.email = 'db@dannyben.com' 12 | s.files = Dir['README.md', 'lib/**/*.*'] 13 | s.homepage = 'https://github.com/DannyBen/webcache' 14 | s.license = 'MIT' 15 | s.required_ruby_version = '>= 3.0' 16 | 17 | s.add_runtime_dependency 'http', '~> 5.0' 18 | s.metadata['rubygems_mfa_required'] = 'true' 19 | end 20 | --------------------------------------------------------------------------------