dis 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cf00e607df1b1aa62ffa3e025c22ac42e3363d14fb0a06cb6b8f41d3b7757db5
4
- data.tar.gz: 2cfe7f5a47d87bdd5e6c59e7f98446bec2918a27c8e3476c123ef1164842d437
3
+ metadata.gz: bf532ee41185ec1d1fb96f36e9cd697a7a1c0ece44da5a30b2ff01d8a6c77010
4
+ data.tar.gz: 8d338d164af992ca38b9af28840c97eabc33f6434791a5505b5a602dd1aa0b3f
5
5
  SHA512:
6
- metadata.gz: e2428a8aa2299b730e1db9702fbec9e725cae6a38bfd3e04bce0ac1ba41484f45840f52cdfe0f998dfe5ffa3c5c5a5fa31bc01c380a1aa8a307392e4a4c9a6a4
7
- data.tar.gz: d1e9bfbc53cdd753fa0b52730b4c6ac88ec00ca1ba3dd519086fdea58f117947f30d0eb5bef297d952a8fb8deab6d642ce939d5a045d19f70f0586c578b3111e
6
+ metadata.gz: 8db87ae2b81b5639fc9312a3dbad3580e6b96886bcb22ddcd6d834efa7452e7348738c37d656ea86c343041cbde683b68f4277a28125f0467de4878b3ca2d3b0
7
+ data.tar.gz: bde07caf197b9a2ae72f3403c3b0f4ad189dbca90259768354eaa96ae7f026edcb3bc6fa86c3b2378361b9eb76ef8a9f5a8446b8584dd5b375861f0de546c740
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2014 Inge Jørgensen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md CHANGED
@@ -3,39 +3,20 @@
3
3
 
4
4
  # Dis
5
5
 
6
- Dis is a content-adressable store for file uploads in your Rails app.
6
+ Dis is a content-addressable store for file uploads in your Rails app.
7
7
 
8
- Data can be stored either on disk or in the cloud - anywhere that
9
- [Fog](http://fog.io) knows how to talk to.
8
+ Data can be stored either on disk or in the cloud anywhere
9
+ [Fog](http://fog.io) can connect to.
10
10
 
11
- It doesn't do any processing, but it's a simple foundation to roll
12
- your own on. If you're looking to handle image uploads, check out
11
+ It doesn't do any processing, but provides a foundation for
12
+ building your own. If you're looking to handle image uploads, check out
13
13
  [DynamicImage](https://github.com/elektronaut/dynamic_image). It's
14
14
  built on top of Dis and handles resizing, cropping and more on demand.
15
15
 
16
- Requires Rails 5+
16
+ ## Requirements
17
17
 
18
- ## Layers
19
-
20
- The underlaying storage consists of one or more layers. A layer is a
21
- unit of storage location, which can either be a local path, or a cloud
22
- provider like Amazon S3 or Google Cloud Storage.
23
-
24
- There are two types of layers, immediate and delayed. Files are
25
- written to immediate layers and then replicated to the rest in the
26
- background using ActiveJob.
27
-
28
- Reads are performed from the first available layer. In case of a read
29
- miss, the file is backfilled from the next layer.
30
-
31
- An example configuration could be to have a local layer first, and
32
- then for example an Amazon S3 bucket. This provides you with an
33
- on-disk cache backed by cloud storage. You can also add additional
34
- layers if you want fault tolerance across regions or even providers.
35
-
36
- Layers can be configured as read-only. This can be useful if you want
37
- to read from your staging or production environment while developing
38
- locally, or if you're transitioning away from a provider.
18
+ - Ruby >= 3.2
19
+ - Rails >= 7.1
39
20
 
40
21
  ## Installation
41
22
 
@@ -51,10 +32,10 @@ Now, run the generator to install the initializer:
51
32
  bin/rails generate dis:install
52
33
  ```
53
34
 
54
- By default, files will be stored in `db/dis`. You can edit
55
- `config/initializers/dis.rb` if you want to change the path or add
56
- additional layers. Note that you also need the corresponding
57
- [Fog gem](https://github.com/fog) if you want to use cloud storage:
35
+ By default, files will be stored in `db/dis`. Edit
36
+ `config/initializers/dis.rb` to change the path or add
37
+ additional layers. Cloud storage requires the corresponding
38
+ [Fog gem](https://github.com/fog):
58
39
 
59
40
  ```ruby
60
41
  gem "fog-aws"
@@ -70,9 +51,9 @@ bin/rails generate dis:model Document
70
51
 
71
52
  This will create a model along with a migration.
72
53
 
73
- Here's what your model might look like. Note that Dis does not
74
- validate any data by default, but you can use the standard Rails validators.
75
- A validator for validating presence of data is provided.
54
+ Here's what your model might look like. Dis does not validate any data
55
+ by default, but you can use standard Rails validators. A presence
56
+ validator for data is also provided.
76
57
 
77
58
  ```ruby
78
59
  class Document < ActiveRecord::Base
@@ -84,30 +65,32 @@ class Document < ActiveRecord::Base
84
65
  end
85
66
  ```
86
67
 
87
- To save your document, simply set the `file` attribute.
68
+ To save your document, set the `file` attribute. This extracts
69
+ `content_type` and `filename` from the upload automatically.
88
70
 
89
71
  ```ruby
90
72
  document_params = params.require(:document).permit(:file)
91
73
  @document = Document.create(document_params)
92
74
  ```
93
75
 
94
- You can also pass a file directly:
76
+ You can also assign `data` directly, but you'll need to set
77
+ `content_type` and `filename` yourself:
95
78
 
96
79
  ```ruby
97
- Document.create(data: File.open('document.pdf'),
98
- content_type: 'application/pdf',
99
- filename: 'document.pdf')
80
+ Document.create(data: File.open("document.pdf"),
81
+ content_type: "application/pdf",
82
+ filename: "document.pdf")
100
83
  ```
101
84
 
102
- ..or even a string:
85
+ ...or even a string:
103
86
 
104
87
  ```ruby
105
- Document.create(data: 'foo', content_type: 'text/plain', filename: 'foo.txt')
88
+ Document.create(data: "foo", content_type: "text/plain", filename: "foo.txt")
106
89
  ```
107
90
 
108
- Getting your file back out is straightforward:
91
+ Reading the file back out:
109
92
 
110
- ``` ruby
93
+ ```ruby
111
94
  class DocumentsController < ApplicationController
112
95
  def show
113
96
  @document = Document.find(params[:id])
@@ -115,15 +98,83 @@ class DocumentsController < ApplicationController
115
98
  send_data(@document.data,
116
99
  filename: @document.filename,
117
100
  type: @document.content_type,
118
- disposition: "attachment)
101
+ disposition: "attachment")
119
102
  end
120
103
  end
121
104
  end
122
105
  ```
123
106
 
124
- ## Behind the scenes
107
+ ## Layers
108
+
109
+ The underlying storage consists of one or more layers. Each layer
110
+ targets either a local path or a cloud provider like Amazon S3 or
111
+ Google Cloud Storage.
112
+
113
+ There are three types of layers:
114
+
115
+ - **Immediate** layers are written to synchronously during the
116
+ request cycle.
117
+ - **Delayed** layers are replicated in the background using ActiveJob.
118
+ - **Cache** layers are bounded, immediate layers with LRU eviction.
119
+ They act as both a read cache and an upload buffer.
120
+
121
+ Reads are performed from the first available layer. On a miss, the
122
+ file is backfilled from the next layer.
123
+
124
+ A typical multi-layer configuration has a local layer first and an
125
+ Amazon S3 bucket second. This gives you an on-disk cache backed by
126
+ cloud storage. Additional layers can provide fault tolerance across
127
+ regions or providers.
128
+
129
+ ```ruby
130
+ # config/initializers/dis.rb
131
+
132
+ # Fast local layer (immediate, synchronous writes)
133
+ Dis::Storage.layers << Dis::Layer.new(
134
+ Fog::Storage.new(provider: "Local", local_root: Rails.root.join("db/dis")),
135
+ path: Rails.env
136
+ )
137
+
138
+ # Cloud layer (delayed, replicated via ActiveJob)
139
+ Dis::Storage.layers << Dis::Layer.new(
140
+ Fog::Storage.new(
141
+ provider: "AWS",
142
+ aws_access_key_id: ENV["AWS_ACCESS_KEY_ID"],
143
+ aws_secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"]
144
+ ),
145
+ path: "my-bucket",
146
+ delayed: true
147
+ )
148
+ ```
149
+
150
+ Layers can be configured as read-only — useful for reading from
151
+ staging or production while developing locally, or when transitioning
152
+ away from a provider.
153
+
154
+ ### Cache layers
155
+
156
+ A cache layer provides bounded local storage with automatic eviction.
157
+ Files are evicted in LRU order, but only after they have been
158
+ replicated to at least one non-cache writeable layer. This ensures
159
+ unreplicated uploads are never lost.
160
+
161
+ The cache size is a soft limit: the cache may temporarily exceed it
162
+ if no files are safe to evict, and will shrink back once delayed
163
+ replication jobs complete.
164
+
165
+ ```ruby
166
+ Dis::Storage.layers << Dis::Layer.new(
167
+ Fog::Storage.new(provider: "Local", local_root: Rails.root.join("tmp/dis")),
168
+ path: Rails.env,
169
+ cache: 1.gigabyte
170
+ )
171
+ ```
172
+
173
+ Cache layers cannot be combined with `delayed` or `readonly`.
174
+
175
+ ## Low-level API
125
176
 
126
- You can interact directly with the store if you want.
177
+ You can also interact with the store directly.
127
178
 
128
179
  ```ruby
129
180
  file = File.open("foo.txt")
@@ -139,23 +190,5 @@ See the [generated documentation on RubyDoc.info](https://www.rubydoc.info/gems/
139
190
 
140
191
  ## License
141
192
 
142
- Copyright 2014 Inge Jørgensen
143
-
144
- Permission is hereby granted, free of charge, to any person obtaining
145
- a copy of this software and associated documentation files (the
146
- "Software"), to deal in the Software without restriction, including
147
- without limitation the rights to use, copy, modify, merge, publish,
148
- distribute, sublicense, and/or sell copies of the Software, and to
149
- permit persons to whom the Software is furnished to do so, subject to
150
- the following conditions:
151
-
152
- The above copyright notice and this permission notice shall be
153
- included in all copies or substantial portions of the Software.
154
-
155
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
156
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
157
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
158
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
159
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
160
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
161
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
193
+ Copyright 2014-2026 Inge Jørgensen. Released under the
194
+ [MIT License](LICENSE).
data/lib/dis/errors.rb CHANGED
@@ -1,15 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dis
4
+ # Namespace for all Dis error classes.
4
5
  module Errors
6
+ # Base error class for all Dis errors.
5
7
  class Error < StandardError; end
6
8
 
9
+ # Raised when attempting to write to a readonly layer.
7
10
  class ReadOnlyError < Dis::Errors::Error; end
8
11
 
12
+ # Raised when no storage layers are configured, or no writeable
13
+ # immediate layers exist for a write operation.
9
14
  class NoLayersError < Dis::Errors::Error; end
10
15
 
16
+ # Raised when a file cannot be found in any storage layer.
11
17
  class NotFoundError < Dis::Errors::Error; end
12
18
 
19
+ # Raised when attempting to store a record that has no data
20
+ # assigned.
13
21
  class NoDataError < Dis::Errors::Error; end
14
22
  end
15
23
  end
@@ -4,14 +4,21 @@ module Dis
4
4
  module Jobs
5
5
  # = Dis ChangeType Job
6
6
  #
7
- # Handles delayed object type change.
7
+ # Handles delayed object type change. Stores the content under
8
+ # the new type in delayed layers, then deletes it under the
9
+ # old type. Retries up to 10 times on failure.
8
10
  #
9
- # Dis::Jobs::ChangeType.perform_later("old_things", "new_things", key)
11
+ # @example
12
+ # Dis::Jobs::ChangeType.perform_later("old", "new", key)
10
13
  class ChangeType < ActiveJob::Base
11
14
  queue_as :dis
12
15
 
13
16
  retry_on StandardError, attempts: 10, wait: :polynomially_longer
14
17
 
18
+ # @param prev_type [String] the current type scope
19
+ # @param new_type [String] the new type scope
20
+ # @param key [String] the content hash
21
+ # @return [void]
15
22
  def perform(prev_type, new_type, key)
16
23
  Dis::Storage.delayed_store(new_type, key)
17
24
  Dis::Storage.delayed_delete(prev_type, key)
@@ -4,14 +4,19 @@ module Dis
4
4
  module Jobs
5
5
  # = Dis Delete Job
6
6
  #
7
- # Handles delayed deletion of objects.
7
+ # Handles delayed deletion of objects from all delayed layers.
8
+ # Retries up to 10 times on failure.
8
9
  #
10
+ # @example
9
11
  # Dis::Jobs::Delete.perform_later("documents", key)
10
12
  class Delete < ActiveJob::Base
11
13
  queue_as :dis
12
14
 
13
15
  retry_on StandardError, attempts: 10, wait: :polynomially_longer
14
16
 
17
+ # @param type [String] the type scope
18
+ # @param key [String] the content hash
19
+ # @return [void]
15
20
  def perform(type, key)
16
21
  Dis::Storage.delayed_delete(type, key)
17
22
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dis
4
+ module Jobs
5
+ # = Dis Evict Job
6
+ #
7
+ # Handles cache eviction for cache layers. Evicts files in LRU
8
+ # order, but only after they have been replicated to a
9
+ # non-cache writeable layer. Retries up to 10 times on failure.
10
+ #
11
+ # @example
12
+ # Dis::Jobs::Evict.perform_later
13
+ class Evict < ActiveJob::Base
14
+ queue_as :dis
15
+
16
+ retry_on StandardError, attempts: 10, wait: :polynomially_longer
17
+
18
+ # @return [void]
19
+ def perform
20
+ Dis::Storage.evict_caches
21
+ end
22
+ end
23
+ end
24
+ end
@@ -4,8 +4,12 @@ module Dis
4
4
  module Jobs
5
5
  # = Dis Store Job
6
6
  #
7
- # Handles delayed storage of objects.
7
+ # Handles delayed storage of objects. Replicates content from
8
+ # immediate layers to all delayed layers. Retries up to 10
9
+ # times on failure. Discarded if the source file no longer
10
+ # exists.
8
11
  #
12
+ # @example
9
13
  # Dis::Jobs::Store.perform_later("documents", key)
10
14
  class Store < ActiveJob::Base
11
15
  queue_as :dis
@@ -14,6 +18,9 @@ module Dis
14
18
 
15
19
  retry_on StandardError, attempts: 10, wait: :polynomially_longer
16
20
 
21
+ # @param type [String] the type scope
22
+ # @param key [String] the content hash
23
+ # @return [void]
17
24
  def perform(type, key)
18
25
  Dis::Storage.delayed_store(type, key)
19
26
  end
data/lib/dis/jobs.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "dis/jobs/delete"
4
+ require "dis/jobs/evict"
4
5
  require "dis/jobs/store"
5
6
  require "dis/jobs/change_type"