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 +4 -4
- data/LICENSE +20 -0
- data/README.md +99 -66
- data/lib/dis/errors.rb +8 -0
- data/lib/dis/jobs/change_type.rb +9 -2
- data/lib/dis/jobs/delete.rb +6 -1
- data/lib/dis/jobs/evict.rb +24 -0
- data/lib/dis/jobs/store.rb +8 -1
- data/lib/dis/jobs.rb +1 -0
- data/lib/dis/layer.rb +174 -55
- data/lib/dis/layers.rb +56 -5
- data/lib/dis/model/class_methods.rb +24 -7
- data/lib/dis/model/data.rb +41 -11
- data/lib/dis/model.rb +43 -17
- data/lib/dis/storage.rb +209 -42
- data/lib/dis/validations/data_presence.rb +9 -1
- data/lib/dis/version.rb +1 -1
- data/lib/dis.rb +14 -1
- data/lib/rails/generators/dis/install/templates/initializer.rb +11 -0
- data/lib/tasks/dis.rake +43 -38
- metadata +27 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf532ee41185ec1d1fb96f36e9cd697a7a1c0ece44da5a30b2ff01d8a6c77010
|
|
4
|
+
data.tar.gz: 8d338d164af992ca38b9af28840c97eabc33f6434791a5505b5a602dd1aa0b3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
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
|
|
9
|
-
[Fog](http://fog.io)
|
|
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
|
|
12
|
-
your own
|
|
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
|
-
|
|
16
|
+
## Requirements
|
|
17
17
|
|
|
18
|
-
|
|
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`.
|
|
55
|
-
`config/initializers/dis.rb`
|
|
56
|
-
additional layers.
|
|
57
|
-
[Fog gem](https://github.com/fog)
|
|
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.
|
|
74
|
-
|
|
75
|
-
|
|
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,
|
|
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
|
|
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(
|
|
98
|
-
content_type:
|
|
99
|
-
filename:
|
|
80
|
+
Document.create(data: File.open("document.pdf"),
|
|
81
|
+
content_type: "application/pdf",
|
|
82
|
+
filename: "document.pdf")
|
|
100
83
|
```
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
...or even a string:
|
|
103
86
|
|
|
104
87
|
```ruby
|
|
105
|
-
Document.create(data:
|
|
88
|
+
Document.create(data: "foo", content_type: "text/plain", filename: "foo.txt")
|
|
106
89
|
```
|
|
107
90
|
|
|
108
|
-
|
|
91
|
+
Reading the file back out:
|
|
109
92
|
|
|
110
|
-
```
|
|
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
|
-
##
|
|
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
|
|
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
|
data/lib/dis/jobs/change_type.rb
CHANGED
|
@@ -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
|
-
#
|
|
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)
|
data/lib/dis/jobs/delete.rb
CHANGED
|
@@ -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
|
data/lib/dis/jobs/store.rb
CHANGED
|
@@ -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
|