dis 0.9.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +165 -0
- data/lib/dis.rb +15 -0
- data/lib/dis/errors.rb +11 -0
- data/lib/dis/jobs.rb +4 -0
- data/lib/dis/jobs/delete.rb +18 -0
- data/lib/dis/jobs/store.rb +18 -0
- data/lib/dis/layer.rb +174 -0
- data/lib/dis/layers.rb +69 -0
- data/lib/dis/model.rb +159 -0
- data/lib/dis/model/class_methods.rb +63 -0
- data/lib/dis/model/data.rb +118 -0
- data/lib/dis/storage.rb +153 -0
- data/lib/dis/validations.rb +3 -0
- data/lib/dis/validations/data_presence.rb +17 -0
- data/lib/dis/version.rb +5 -0
- data/lib/rails/generators/dis/install/install_generator.rb +17 -0
- data/lib/rails/generators/dis/install/templates/initializer.rb +18 -0
- data/lib/rails/generators/dis/model/model_generator.rb +41 -0
- metadata +134 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d9ec3b8500386221e40c1ea04e0b38e0d416f6d
|
4
|
+
data.tar.gz: 1b7ae3618b80c6d95b27a5ba832292c3cd035a45
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 186c74e43812f0e71ddfc36856106decb829607854bd819d5a51467cfeb3e19811fabd9bd755975bfdc50ee11971e96f19a847612da6e0aaf456078a453916fe
|
7
|
+
data.tar.gz: 54bc48092d38eca8edb24d727b5295c56ea9a7d31aed80432ac8ea4a9316eb14162d2333797c76ae8b280a5d55e5fcb4ab27c7652e221d2de1b486e6385b21cf
|
data/MIT-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
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# Dis [](https://travis-ci.org/elektronaut/dis) [](https://codeclimate.com/github/elektronaut/dis) [](https://codeclimate.com/github/elektronaut/dis)
|
2
|
+
|
3
|
+
Dis handles file uploads for your Rails app.
|
4
|
+
It's similar to [Paperclip](https://github.com/thoughtbot/paperclip)
|
5
|
+
and [Carrierwave](https://github.com/carrierwaveuploader/carrierwave),
|
6
|
+
but different in a few ways. Chiefly, it's much, much simpler.
|
7
|
+
|
8
|
+
Your files are stored in one or more layers, either on disk or in
|
9
|
+
a cloud somewhere. [Fog](http://fog.io) and
|
10
|
+
[Active Job](https://github.com/rails/activejob) does most of the
|
11
|
+
heavy lifting.
|
12
|
+
|
13
|
+
Files are indexed by the SHA1 hash of their contents. This means you get
|
14
|
+
deduplication for free. This also means you run the (very slight) risk of
|
15
|
+
hash collisions. There is no concept of updates in the data store,
|
16
|
+
a file with changed content is by definition a different file.
|
17
|
+
|
18
|
+
It does not do any processing. The idea is to provide a simple foundation
|
19
|
+
other gems can build on. If you are looking to handle uploaded images,
|
20
|
+
check out [DynamicImage](https://github.com/elektronaut/dynamic_image).
|
21
|
+
|
22
|
+
Requires Rails 4.1+ and Ruby 1.9.3+.
|
23
|
+
|
24
|
+
## Documentation
|
25
|
+
|
26
|
+
[Documentation on RubyDoc.info](http://rdoc.info/github/elektronaut/dis)
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add the gem to your Gemfile and run `bundle install`:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem "dis"
|
34
|
+
```
|
35
|
+
|
36
|
+
Now, run the generator to install the initializer:
|
37
|
+
|
38
|
+
```sh
|
39
|
+
bin/rails generate dis:install
|
40
|
+
```
|
41
|
+
|
42
|
+
## Usage
|
43
|
+
|
44
|
+
Run the generator to create your model.
|
45
|
+
|
46
|
+
```sh
|
47
|
+
bin/rails generate dis:model Document
|
48
|
+
```
|
49
|
+
|
50
|
+
This will create a model along with a migration.
|
51
|
+
|
52
|
+
Here's what your model might look like. Note that Dis does not
|
53
|
+
validate any data by default, you are expected to use the Rails validators.
|
54
|
+
A validator for validating presence of data is provided.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class Document < ActiveRecord::Base
|
58
|
+
include Dis::Model
|
59
|
+
validates_data_presence
|
60
|
+
validates :content_type, presence: true, format: /\Aapplication\/(x\-)?pdf\z/
|
61
|
+
validates :filename, presence: true, format: /\A[\w_\-\.]+\.pdf\z/i
|
62
|
+
validates :content_length, numericality: { less_than: 5.megabytes }
|
63
|
+
end
|
64
|
+
```
|
65
|
+
|
66
|
+
To save your document, simply set the `file` attribute.
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
document_params = params.require(:document).permit(:file)
|
70
|
+
@document = Document.create(document_params)
|
71
|
+
```
|
72
|
+
|
73
|
+
You can also assign the data directly.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
Document.create(
|
77
|
+
data: File.open('document.pdf'),
|
78
|
+
content_type: 'application/pdf',
|
79
|
+
filename: 'document.pdf'
|
80
|
+
)
|
81
|
+
```
|
82
|
+
|
83
|
+
## Defining layers
|
84
|
+
|
85
|
+
The install generator will set you up with a local storage layer on disk,
|
86
|
+
but this is configurable in `config/initializers/dis.rb`.
|
87
|
+
|
88
|
+
You can have as many layers as you want, any storage provider
|
89
|
+
[supported by Fog](http://fog.io/storage/) should work in theory.
|
90
|
+
Having a local layer first is a good idea, this will provide you
|
91
|
+
with a cache on disk. Any misses will be filled from the next layer.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
Dis::Storage.layers << Dis::Layer.new(
|
95
|
+
Fog::Storage.new({provider: 'Local', local_root: Rails.root.join('db', 'binaries')}),
|
96
|
+
path: Rails.env
|
97
|
+
)
|
98
|
+
```
|
99
|
+
|
100
|
+
Delayed layers will be processed out of the request cycle using
|
101
|
+
whatever adapter you've configured
|
102
|
+
[Active Job](https://github.com/rails/activejob) to use.
|
103
|
+
Note: You must have at least one non-delayed layer.
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
if Rails.env.production?
|
107
|
+
Dis::Storage.layers << Dis::Layer.new(
|
108
|
+
Fog::Storage.new({
|
109
|
+
provider: 'AWS',
|
110
|
+
aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID,
|
111
|
+
aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
112
|
+
}),
|
113
|
+
path: "my_bucket",
|
114
|
+
delayed: true
|
115
|
+
)
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
You can also set layers to be read only. This is handy if you want to
|
120
|
+
access production data from your development environment, or if you
|
121
|
+
are in the process of migration from one provider to another.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
if Rails.env.development?
|
125
|
+
Dis::Storage.layers << Dis::Layer.new(
|
126
|
+
Fog::Storage.new(...),
|
127
|
+
readonly: true
|
128
|
+
)
|
129
|
+
end
|
130
|
+
```
|
131
|
+
|
132
|
+
## Interacting with the store
|
133
|
+
|
134
|
+
You can interact directly with the store if you want.
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
file = File.open("foo.txt")
|
138
|
+
hash = Dis::Storage.store("stuff", file) # => "8843d7f92416211de9ebb963ff4ce28125932878"
|
139
|
+
Dis::Storage.exists?("stuff", hash) # => true
|
140
|
+
Dis::Storage.get("stuff", hash).body # => "foobar"
|
141
|
+
Dis::Storage.delete("stuff", hash) # => true
|
142
|
+
```
|
143
|
+
|
144
|
+
## License
|
145
|
+
|
146
|
+
Copyright 2014 Inge Jørgensen
|
147
|
+
|
148
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
149
|
+
a copy of this software and associated documentation files (the
|
150
|
+
"Software"), to deal in the Software without restriction, including
|
151
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
152
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
153
|
+
permit persons to whom the Software is furnished to do so, subject to
|
154
|
+
the following conditions:
|
155
|
+
|
156
|
+
The above copyright notice and this permission notice shall be
|
157
|
+
included in all copies or substantial portions of the Software.
|
158
|
+
|
159
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
160
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
161
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
162
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
163
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
164
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
165
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/lib/dis.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
require "fog"
|
5
|
+
require "active_job"
|
6
|
+
require "dis/errors"
|
7
|
+
require "dis/jobs"
|
8
|
+
require "dis/layer"
|
9
|
+
require "dis/layers"
|
10
|
+
require "dis/model"
|
11
|
+
require "dis/storage"
|
12
|
+
require "dis/validations"
|
13
|
+
|
14
|
+
module Dis
|
15
|
+
end
|
data/lib/dis/errors.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Errors
|
5
|
+
class Error < StandardError; end
|
6
|
+
class ReadOnlyError < Dis::Errors::Error; end
|
7
|
+
class NoLayersError < Dis::Errors::Error; end
|
8
|
+
class NotFoundError < Dis::Errors::Error; end
|
9
|
+
class NoDataError < Dis::Errors::Error; end
|
10
|
+
end
|
11
|
+
end
|
data/lib/dis/jobs.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Jobs
|
5
|
+
# = Dis Delete Job
|
6
|
+
#
|
7
|
+
# Handles delayed deletion of objects.
|
8
|
+
#
|
9
|
+
# Dis::Jobs::Delete.enqueue("documents", hash)
|
10
|
+
class Delete < ActiveJob::Base
|
11
|
+
queue_as :dis
|
12
|
+
|
13
|
+
def perform(type, hash)
|
14
|
+
Dis::Storage.delayed_delete(type, hash)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Jobs
|
5
|
+
# = Dis Store Job
|
6
|
+
#
|
7
|
+
# Handles delayed storage of objects.
|
8
|
+
#
|
9
|
+
# Dis::Jobs::Store.enqueue("documents", hash)
|
10
|
+
class Store < ActiveJob::Base
|
11
|
+
queue_as :dis
|
12
|
+
|
13
|
+
def perform(type, hash)
|
14
|
+
Dis::Storage.delayed_store(type, hash)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/dis/layer.rb
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
# = Dis Layer
|
5
|
+
#
|
6
|
+
# Represents a layer of storage. It's a wrapper around
|
7
|
+
# <tt>Fog::Storage</tt>, any provider supported by Fog should be usable.
|
8
|
+
#
|
9
|
+
# ==== Options
|
10
|
+
#
|
11
|
+
# * <tt>:delayed</tt> - Delayed layers will be processed outside of
|
12
|
+
# the request cycle by ActiveJob.
|
13
|
+
# * <tt>:readonly</tt> - Readonly layers can only be read from,
|
14
|
+
# not written to.
|
15
|
+
# * <tt>:public</tt> - Objects stored in public layers will have the
|
16
|
+
# public readable flag set if supported by the storage provider.
|
17
|
+
# * <tt>:path</tt> - Directory name to use for the store. For Amazon S3,
|
18
|
+
# this will be the name of the bucket.
|
19
|
+
#
|
20
|
+
# ==== Examples
|
21
|
+
#
|
22
|
+
# This creates a local storage layer. It's a good idea to have a local layer
|
23
|
+
# first, this provides you with a cache on disk that will be faster than
|
24
|
+
# reading from the cloud.
|
25
|
+
#
|
26
|
+
# Dis::Layer.new(
|
27
|
+
# Fog::Storage.new({
|
28
|
+
# provider: 'Local',
|
29
|
+
# local_root: Rails.root.join('db', 'dis')
|
30
|
+
# }),
|
31
|
+
# path: Rails.env
|
32
|
+
# )
|
33
|
+
#
|
34
|
+
# This creates a delayed layer on Amazon S3. ActiveJob will kick in and
|
35
|
+
# and transfer content from one of the immediate layers later at it's
|
36
|
+
# leisure.
|
37
|
+
#
|
38
|
+
# Dis::Layer.new(
|
39
|
+
# Fog::Storage.new({
|
40
|
+
# provider: 'AWS',
|
41
|
+
# aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID,
|
42
|
+
# aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
|
43
|
+
# }),
|
44
|
+
# path: "my_bucket",
|
45
|
+
# delayed: true
|
46
|
+
# )
|
47
|
+
class Layer
|
48
|
+
attr_reader :connection
|
49
|
+
|
50
|
+
def initialize(connection, options={})
|
51
|
+
options = default_options.merge(options)
|
52
|
+
@connection = connection
|
53
|
+
@delayed = options[:delayed]
|
54
|
+
@readonly = options[:readonly]
|
55
|
+
@public = options[:public]
|
56
|
+
@path = options[:path]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns true if the layer is a delayed layer.
|
60
|
+
def delayed?
|
61
|
+
@delayed
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if the layer isn't a delayed layer.
|
65
|
+
def immediate?
|
66
|
+
!delayed?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns true if the layer isn't a delayed layer.
|
70
|
+
def public?
|
71
|
+
@public
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true if the layer is read only.
|
75
|
+
def readonly?
|
76
|
+
@readonly
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if the layer is writeable.
|
80
|
+
def writeable?
|
81
|
+
!readonly?
|
82
|
+
end
|
83
|
+
|
84
|
+
# Stores a file.
|
85
|
+
#
|
86
|
+
# hash = Digest::SHA1.file(file.path).hexdigest
|
87
|
+
# layer.store("documents", hash, path)
|
88
|
+
#
|
89
|
+
# Hash must be a hex digest of the file content. If an object with the
|
90
|
+
# supplied hash already exists, no action will be performed. In other
|
91
|
+
# words, no data will be overwritten if a hash collision occurs.
|
92
|
+
#
|
93
|
+
# Returns an instance of Fog::Model, or raises an error if the layer
|
94
|
+
# is readonly.
|
95
|
+
def store(type, hash, file)
|
96
|
+
raise Dis::Errors::ReadOnlyError if readonly?
|
97
|
+
store!(type, hash, file)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns true if a object with the given hash exists.
|
101
|
+
#
|
102
|
+
# layer.exists?("documents", hash)
|
103
|
+
def exists?(type, hash)
|
104
|
+
(directory(type, hash) &&
|
105
|
+
directory(type, hash).files.head(key_component(type, hash))) ? true : false
|
106
|
+
end
|
107
|
+
|
108
|
+
# Retrieves a file from the store.
|
109
|
+
#
|
110
|
+
# layer.get("documents", hash)
|
111
|
+
def get(type, hash)
|
112
|
+
if dir = directory(type, hash)
|
113
|
+
dir.files.get(key_component(type, hash))
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Deletes a file from the store.
|
118
|
+
#
|
119
|
+
# layer.delete("documents", hash)
|
120
|
+
#
|
121
|
+
# Returns true if the file was deleted, or false if it could not be found.
|
122
|
+
# Raises an error if the layer is readonly.
|
123
|
+
def delete(type, hash)
|
124
|
+
raise Dis::Errors::ReadOnlyError if readonly?
|
125
|
+
delete!(type, hash)
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def default_options
|
131
|
+
{ delayed: false, readonly: false, public: false, path: nil }
|
132
|
+
end
|
133
|
+
|
134
|
+
def directory_component(type, hash)
|
135
|
+
path || ""
|
136
|
+
end
|
137
|
+
|
138
|
+
def key_component(type, hash)
|
139
|
+
[type, hash[0...2], hash[2..hash.length]].compact.join('/')
|
140
|
+
end
|
141
|
+
|
142
|
+
def delete!(type, hash)
|
143
|
+
return false unless exists?(type, hash)
|
144
|
+
get(type, hash).destroy
|
145
|
+
end
|
146
|
+
|
147
|
+
def directory(type, hash)
|
148
|
+
connection.directories.get(directory_component(type, hash))
|
149
|
+
end
|
150
|
+
|
151
|
+
def directory!(type, hash)
|
152
|
+
dir = directory(type, hash)
|
153
|
+
dir ||= connection.directories.create(
|
154
|
+
key: directory_component(type, hash),
|
155
|
+
public: public?
|
156
|
+
)
|
157
|
+
dir
|
158
|
+
end
|
159
|
+
|
160
|
+
def store!(type, hash, file)
|
161
|
+
return get(type, hash) if exists?(type, hash)
|
162
|
+
file.rewind if file.respond_to?(:rewind)
|
163
|
+
directory!(type, hash).files.create(
|
164
|
+
key: key_component(type, hash),
|
165
|
+
body: (file.kind_of?(Fog::Model) ? file.body : file),
|
166
|
+
public: public?
|
167
|
+
)
|
168
|
+
end
|
169
|
+
|
170
|
+
def path
|
171
|
+
@path && !@path.empty? ? @path : nil
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/dis/layers.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
# = Dis Layers
|
5
|
+
#
|
6
|
+
# Represents a collection of layers.
|
7
|
+
class Layers
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
def initialize(layers=[])
|
11
|
+
@layers = layers
|
12
|
+
end
|
13
|
+
|
14
|
+
# Adds a layer to the collection.
|
15
|
+
def <<(layer)
|
16
|
+
@layers << layer
|
17
|
+
end
|
18
|
+
|
19
|
+
# Clears all layers from the collection.
|
20
|
+
def clear!
|
21
|
+
@layers = []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Iterates over the layers.
|
25
|
+
def each
|
26
|
+
@layers.each { |layer| yield layer }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns a new instance containing only the delayed layers.
|
30
|
+
def delayed
|
31
|
+
self.class.new select { |layer| layer.delayed? }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns true if one or more delayed layers exist.
|
35
|
+
def delayed?
|
36
|
+
delayed.any?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a new instance containing only the immediate layers.
|
40
|
+
def immediate
|
41
|
+
self.class.new select { |layer| layer.immediate? }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns true if one or more immediate layers exist.
|
45
|
+
def immediate?
|
46
|
+
immediate.any?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a new instance containing only the readonly layers.
|
50
|
+
def readonly
|
51
|
+
self.class.new select { |layer| layer.readonly? }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns true if one or more readonly layers exist.
|
55
|
+
def readonly?
|
56
|
+
readonly.any?
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns a new instance containing only the writeable layers.
|
60
|
+
def writeable
|
61
|
+
self.class.new select { |layer| layer.writeable? }
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true if one or more writeable layers exist.
|
65
|
+
def writeable?
|
66
|
+
writeable.any?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/dis/model.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'dis/model/class_methods'
|
4
|
+
require 'dis/model/data'
|
5
|
+
|
6
|
+
module Dis
|
7
|
+
# = Dis Model
|
8
|
+
#
|
9
|
+
# ActiveModel extension for the model holding your data. To use it,
|
10
|
+
# simply include the module in your model:
|
11
|
+
#
|
12
|
+
# class Document < ActiveRecord::Base
|
13
|
+
# include Dis::Model
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# You'll need to define a few attributes in your database table.
|
17
|
+
# Here's a minimal migration:
|
18
|
+
#
|
19
|
+
# create_table :documents do |t|
|
20
|
+
# t.string :content_hash
|
21
|
+
# t.string :content_type
|
22
|
+
# t.integer :content_length
|
23
|
+
# t.string :filename
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# You can override the names of any of these by setting
|
27
|
+
# <tt>dis_attributes</tt>.
|
28
|
+
#
|
29
|
+
# class Document < ActiveRecord::Base
|
30
|
+
# include Dis::Model
|
31
|
+
# self.dis_attributes = {
|
32
|
+
# filename: :my_filename,
|
33
|
+
# content_length: :filesize
|
34
|
+
# }
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# == Usage
|
38
|
+
#
|
39
|
+
# To save a file, simply assign to the <tt>file</tt> attribute.
|
40
|
+
#
|
41
|
+
# document = Document.create(file: params.permit(:file))
|
42
|
+
#
|
43
|
+
# <tt>content_type</tt> and <tt>filename</tt> will automatically be set if
|
44
|
+
# the supplied object quacks like a file. <tt>content_length</tt> will always
|
45
|
+
# be set. <tt>content_hash</tt> won't be set until the record is being saved.
|
46
|
+
#
|
47
|
+
# If you don't care about filenames and content types and just want to store
|
48
|
+
# a binary blob, you can also just set the <tt>data</tt> attribute.
|
49
|
+
#
|
50
|
+
# my_data = File.read('document.pdf')
|
51
|
+
# document.update(data: my_data)
|
52
|
+
#
|
53
|
+
# The data won't be stored until the record is saved, and not unless
|
54
|
+
# the record is valid.
|
55
|
+
#
|
56
|
+
# To retrieve your data, simply read the <tt>data</tt> attribute. The file
|
57
|
+
# will be lazily loaded from the store on demand and cached in memory as long
|
58
|
+
# as the record stays in scope.
|
59
|
+
#
|
60
|
+
# my_data = document.data
|
61
|
+
#
|
62
|
+
# Destroying a record will delete the file from the store, unless another
|
63
|
+
# record also refers to the same hash. Similarly, stale files will be purged
|
64
|
+
# when content changes.
|
65
|
+
#
|
66
|
+
# == Validations
|
67
|
+
#
|
68
|
+
# No validation is performed by default. If you want to ensure that data is
|
69
|
+
# present, use the <tt>validates_data_presence</tt> method.
|
70
|
+
#
|
71
|
+
# class Document < ActiveRecord::Base
|
72
|
+
# include Dis::Model
|
73
|
+
# validates_data_presence
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# If you want to validate content types, size or similar, simply use standard
|
77
|
+
# Rails validations on the metadata attributes:
|
78
|
+
#
|
79
|
+
# validates :content_type, presence: true, format: /\Aapplication\/(x\-)?pdf\z/
|
80
|
+
# validates :filename, presence: true, format: /\A[\w_\-\.]+\.pdf\z/i
|
81
|
+
# validates :content_length, numericality: { less_than: 5.megabytes }
|
82
|
+
module Model
|
83
|
+
extend ActiveSupport::Concern
|
84
|
+
|
85
|
+
included do
|
86
|
+
before_save :store_data
|
87
|
+
after_save :cleanup_data
|
88
|
+
after_destroy :delete_data
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns the data as a binary string, or nil if no data has been set.
|
92
|
+
def data
|
93
|
+
dis_data.read
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns true if data is set.
|
97
|
+
def data?
|
98
|
+
dis_data.any?
|
99
|
+
end
|
100
|
+
|
101
|
+
# Assigns new data. This also sets <tt>content_length</tt>, and resets
|
102
|
+
# <tt>content_hash</tt> to nil.
|
103
|
+
def data=(new_data)
|
104
|
+
new_data = Dis::Model::Data.new(self, new_data)
|
105
|
+
attribute_will_change!('data') unless new_data == dis_data
|
106
|
+
@dis_data = new_data
|
107
|
+
dis_set :content_hash, nil
|
108
|
+
dis_set :content_length, dis_data.content_length
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns true if the data has been changed since the object was last saved.
|
112
|
+
def data_changed?
|
113
|
+
changes.include?('data')
|
114
|
+
end
|
115
|
+
|
116
|
+
# Assigns new data from an uploaded file. In addition to the actions
|
117
|
+
# performed by <tt>data=</tt>, this will set <tt>content_type</tt> and
|
118
|
+
# <tt>filename</tt>.
|
119
|
+
def file=(file)
|
120
|
+
self.data = file
|
121
|
+
dis_set :content_type, file.content_type
|
122
|
+
dis_set :filename, file.original_filename
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def cleanup_data
|
128
|
+
if previous_hash = changes[dis_attribute(:content_hash)].try(&:first)
|
129
|
+
dis_data.expire(previous_hash)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def delete_data
|
134
|
+
dis_data.expire(dis_get(:content_hash))
|
135
|
+
end
|
136
|
+
|
137
|
+
def store_data
|
138
|
+
if dis_data.changed?
|
139
|
+
dis_set :content_hash, dis_data.store!
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def dis_get(attribute_name)
|
144
|
+
self[dis_attribute(attribute_name)]
|
145
|
+
end
|
146
|
+
|
147
|
+
def dis_data
|
148
|
+
@dis_data ||= Dis::Model::Data.new(self)
|
149
|
+
end
|
150
|
+
|
151
|
+
def dis_set(attribute_name, value)
|
152
|
+
self[dis_attribute(attribute_name)] = value
|
153
|
+
end
|
154
|
+
|
155
|
+
def dis_attribute(attribute_name)
|
156
|
+
self.class.dis_attributes[attribute_name]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Model
|
5
|
+
module ClassMethods
|
6
|
+
# Returns the mapping of attribute names.
|
7
|
+
def dis_attributes
|
8
|
+
default_dis_attributes.merge(@dis_attributes || {})
|
9
|
+
end
|
10
|
+
|
11
|
+
# Sets the current mapping of attribute names. Use this if you want to
|
12
|
+
# override the attributes and database columns that Dis will use.
|
13
|
+
#
|
14
|
+
# class Document < ActiveRecord::Base
|
15
|
+
# include Dis::Model
|
16
|
+
# self.dis_attributes = { filename: :my_custom_filename }
|
17
|
+
# end
|
18
|
+
def dis_attributes=(new_attributes)
|
19
|
+
@dis_attributes = new_attributes
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the storage type name, which Dis will use for
|
23
|
+
# directory scoping. Defaults to the name of the database table.
|
24
|
+
#
|
25
|
+
# class Document < ActiveRecord::Base; end
|
26
|
+
# Document.dis_type # => "documents"
|
27
|
+
def dis_type
|
28
|
+
@dis_type || self.table_name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Sets the storage type name.
|
32
|
+
#
|
33
|
+
# Take care not to set the same name for multiple models, this will
|
34
|
+
# cause data loss when a record is destroyed.
|
35
|
+
def dis_type=(new_type)
|
36
|
+
@dis_type = new_type
|
37
|
+
end
|
38
|
+
|
39
|
+
# Adds a presence validation on the +data+ attribute.
|
40
|
+
#
|
41
|
+
# This is better than using `validates :data, presence: true`, since
|
42
|
+
# that would cause it to load the data from storage on each save.
|
43
|
+
#
|
44
|
+
# class Document < ActiveRecord::Base
|
45
|
+
# include Dis::Model
|
46
|
+
# validates_data_presence
|
47
|
+
# end
|
48
|
+
def validates_data_presence
|
49
|
+
validates_with Dis::Validations::DataPresence
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the default attribute names.
|
53
|
+
def default_dis_attributes
|
54
|
+
{
|
55
|
+
content_hash: :content_hash,
|
56
|
+
content_length: :content_length,
|
57
|
+
content_type: :content_type,
|
58
|
+
filename: :filename
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Model
|
5
|
+
# = Dis Model Data
|
6
|
+
#
|
7
|
+
# Facilitates communication between the model and the storage,
|
8
|
+
# and holds any newly assigned data before the record is saved.
|
9
|
+
class Data
|
10
|
+
def initialize(record, raw=nil)
|
11
|
+
@record = record
|
12
|
+
@raw = raw
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns true if two Data objects represent the same data.
|
16
|
+
def ==(comp)
|
17
|
+
# TODO: This can be made faster by
|
18
|
+
# comparing hashes for stored objects.
|
19
|
+
comp.read == read
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns true if data exists either in memory or in storage.
|
23
|
+
def any?
|
24
|
+
raw? || stored?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the data as a binary string.
|
28
|
+
def read
|
29
|
+
@cached ||= read_from(closest)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Will be true if data has been explicitely set.
|
33
|
+
#
|
34
|
+
# Dis::Model::Data.new(record).changed? # => false
|
35
|
+
# Dis::Model::Data.new(record, new_file).changed? # => true
|
36
|
+
def changed?
|
37
|
+
raw?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the length of the data.
|
41
|
+
def content_length
|
42
|
+
if raw? && raw.respond_to?(:length)
|
43
|
+
raw.length
|
44
|
+
else
|
45
|
+
read.try(&:length).to_i
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Expires a data object from the storage if it's no longer being used
|
50
|
+
# by existing records. This is triggered from callbacks on the record
|
51
|
+
# whenever they are changed or destroyed.
|
52
|
+
def expire(hash)
|
53
|
+
unless @record.class.where(
|
54
|
+
@record.class.dis_attributes[:content_hash] => hash
|
55
|
+
).any?
|
56
|
+
Dis::Storage.delete(storage_type, hash)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Stores the data. Returns a hash of the content for reference.
|
61
|
+
def store!
|
62
|
+
raise Dis::Errors::NoDataError unless raw?
|
63
|
+
Dis::Storage.store(storage_type, raw)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def closest
|
69
|
+
if raw?
|
70
|
+
raw
|
71
|
+
elsif stored?
|
72
|
+
stored
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def content_hash
|
77
|
+
@record[@record.class.dis_attributes[:content_hash]]
|
78
|
+
end
|
79
|
+
|
80
|
+
def raw?
|
81
|
+
raw ? true : false
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_from(object)
|
85
|
+
return nil unless object
|
86
|
+
if object.respond_to?(:body)
|
87
|
+
object.body
|
88
|
+
elsif object.respond_to?(:read)
|
89
|
+
object.rewind
|
90
|
+
response = object.read
|
91
|
+
object.rewind
|
92
|
+
response
|
93
|
+
else
|
94
|
+
object
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def storage_type
|
99
|
+
@record.class.dis_type
|
100
|
+
end
|
101
|
+
|
102
|
+
def stored?
|
103
|
+
!content_hash.blank?
|
104
|
+
end
|
105
|
+
|
106
|
+
def stored
|
107
|
+
Dis::Storage.get(
|
108
|
+
storage_type,
|
109
|
+
content_hash
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
def raw
|
114
|
+
@raw
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/lib/dis/storage.rb
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
# = Dis Storage
|
5
|
+
#
|
6
|
+
# This is the interface for interacting with the storage layers.
|
7
|
+
#
|
8
|
+
# All queries are scoped by object type, which will default to the table
|
9
|
+
# name of the model. Take care to use your own scope if you interact with
|
10
|
+
# the store directly, as models will purge expired content when they change.
|
11
|
+
#
|
12
|
+
# Files are stored with a SHA1 digest of the file contents as the key.
|
13
|
+
# This ensures data is deduplicated per scope. Hash collisions will be
|
14
|
+
# silently ignored.
|
15
|
+
#
|
16
|
+
# Layers should be added to <tt>Dis::Storage.layers</tt>. At least
|
17
|
+
# one writeable, non-delayed layer must exist.
|
18
|
+
class Storage
|
19
|
+
class << self
|
20
|
+
# Exposes the layer set, which is an instance of
|
21
|
+
# <tt>Dis::Layers</tt>.
|
22
|
+
def layers
|
23
|
+
@layers ||= Dis::Layers.new
|
24
|
+
end
|
25
|
+
|
26
|
+
# Stores a file and returns a digest. Kicks off a
|
27
|
+
# <tt>Dis::Jobs::Store</tt> job if any delayed layers are defined.
|
28
|
+
#
|
29
|
+
# hash = Dis::Storage.store("things", File.open('foo.bin'))
|
30
|
+
# # => "8843d7f92416211de9ebb963ff4ce28125932878"
|
31
|
+
def store(type, file)
|
32
|
+
require_writeable_layers!
|
33
|
+
hash = store_immediately!(type, file)
|
34
|
+
if layers.delayed.writeable.any?
|
35
|
+
Dis::Jobs::Store.enqueue(type, hash)
|
36
|
+
end
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
|
40
|
+
# Transfers files from immediate layers to all delayed layers.
|
41
|
+
#
|
42
|
+
# Dis::Storage.delayed_store("things", hash)
|
43
|
+
def delayed_store(type, hash)
|
44
|
+
file = get(type, hash)
|
45
|
+
layers.delayed.writeable.each do |layer|
|
46
|
+
layer.store(type, hash, file)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns true if the file exists in any layer.
|
51
|
+
#
|
52
|
+
# Dis::Storage.exists?("things", hash) # => true
|
53
|
+
def exists?(type, hash)
|
54
|
+
require_layers!
|
55
|
+
layers.each do |layer|
|
56
|
+
return true if layer.exists?(type, hash)
|
57
|
+
end
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
# Retrieves a file from the store.
|
62
|
+
#
|
63
|
+
# stuff = Dis::Storage.get("things", hash)
|
64
|
+
#
|
65
|
+
# If any misses are detected, it will try to fetch the file from the
|
66
|
+
# first available layer, then store it in all immediate layer.
|
67
|
+
#
|
68
|
+
# Returns an instance of Fog::Model.
|
69
|
+
def get(type, hash)
|
70
|
+
require_layers!
|
71
|
+
miss = false
|
72
|
+
layers.each do |layer|
|
73
|
+
if result = layer.get(type, hash)
|
74
|
+
store_immediately!(type, result) if miss
|
75
|
+
return result
|
76
|
+
else
|
77
|
+
miss = true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
raise Dis::Errors::NotFoundError
|
81
|
+
end
|
82
|
+
|
83
|
+
# Deletes a file from all layers. Kicks off a
|
84
|
+
# <tt>Dis::Jobs::Delete</tt> job if any delayed layers are defined.
|
85
|
+
# Returns true if the file existed in any immediate layers,
|
86
|
+
# or false if not.
|
87
|
+
#
|
88
|
+
# Dis::Storage.delete("things", hash)
|
89
|
+
# # => true
|
90
|
+
# Dis::Storage.delete("things", hash)
|
91
|
+
# # => false
|
92
|
+
def delete(type, hash)
|
93
|
+
require_writeable_layers!
|
94
|
+
deleted = false
|
95
|
+
layers.immediate.writeable.each do |layer|
|
96
|
+
deleted = true if layer.delete(type, hash)
|
97
|
+
end
|
98
|
+
if layers.delayed.writeable.any?
|
99
|
+
Dis::Jobs::Delete.enqueue(type, hash)
|
100
|
+
end
|
101
|
+
deleted
|
102
|
+
end
|
103
|
+
|
104
|
+
# Deletes content from all delayed layers.
|
105
|
+
#
|
106
|
+
# Dis::Storage.delayed_delete("things", hash)
|
107
|
+
def delayed_delete(type, hash)
|
108
|
+
layers.delayed.writeable.each do |layer|
|
109
|
+
layer.delete(type, hash)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def store_immediately!(type, file)
|
116
|
+
hash_file(file) do |hash|
|
117
|
+
layers.immediate.writeable.each do |layer|
|
118
|
+
layer.store(type, hash, file)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def require_layers!
|
124
|
+
unless layers.any?
|
125
|
+
raise Dis::Errors::NoLayersError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def require_writeable_layers!
|
130
|
+
unless layers.immediate.writeable.any?
|
131
|
+
raise Dis::Errors::NoLayersError
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def digest
|
136
|
+
Digest::SHA1
|
137
|
+
end
|
138
|
+
|
139
|
+
def hash_file(file, &block)
|
140
|
+
hash = case file
|
141
|
+
when Fog::Model
|
142
|
+
digest.hexdigest(file.body)
|
143
|
+
when String
|
144
|
+
digest.hexdigest(file)
|
145
|
+
else
|
146
|
+
digest.file(file.path).hexdigest
|
147
|
+
end
|
148
|
+
yield hash if block_given?
|
149
|
+
hash
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Dis
|
4
|
+
module Validations
|
5
|
+
# = Dis Data Presence Validation
|
6
|
+
#
|
7
|
+
class DataPresence < ActiveModel::Validator
|
8
|
+
# Validates that a record has data, either freshly assigned or
|
9
|
+
# persisted in the storage. Adds a `:blank` error on `:data`if not.
|
10
|
+
def validate(record)
|
11
|
+
unless record.data?
|
12
|
+
record.errors.add(:data, :blank)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/dis/version.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/rails/model/model_generator'
|
5
|
+
|
6
|
+
module Dis
|
7
|
+
module Generators
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
desc "Creates the Dis initializer"
|
10
|
+
source_root File.expand_path("../templates", __FILE__)
|
11
|
+
|
12
|
+
def create_initializer
|
13
|
+
template 'initializer.rb', File.join('config', 'initializers', 'dis.rb')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Be sure to restart your server when you modify this file.
|
2
|
+
|
3
|
+
# Creates a local storage layer in db/dis:
|
4
|
+
Dis::Storage.layers << Dis::Layer.new(
|
5
|
+
Fog::Storage.new({ provider: 'Local', local_root: Rails.root.join('db', 'dis') }),
|
6
|
+
path: Rails.env
|
7
|
+
)
|
8
|
+
|
9
|
+
# You can also add cloud storage.
|
10
|
+
# Dis::Storage.layers << Dis::Layer.new(
|
11
|
+
# Fog::Storage.new({
|
12
|
+
# provider: 'AWS',
|
13
|
+
# aws_access_key_id: AWS_ACCESS_KEY_ID,
|
14
|
+
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
|
15
|
+
# }),
|
16
|
+
# path: "my_bucket",
|
17
|
+
# delayed: true
|
18
|
+
# )
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
require 'rails/generators/rails/model/model_generator'
|
5
|
+
|
6
|
+
module Dis
|
7
|
+
module Generators
|
8
|
+
class ModelGenerator < Rails::Generators::ModelGenerator
|
9
|
+
desc "Creates a Dis model"
|
10
|
+
|
11
|
+
def initialize(args, *options)
|
12
|
+
super(inject_dis_attributes(args), *options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_model_extension
|
16
|
+
inject_into_file File.join('app/models', class_path, "#{file_name}.rb"), after: "ActiveRecord::Base\n" do
|
17
|
+
" include Dis::Model\n"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def inject_dis_attributes(args)
|
24
|
+
if args.any?
|
25
|
+
args = [args[0]] + dis_attributes + args[1..args.length]
|
26
|
+
else
|
27
|
+
args
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def dis_attributes
|
32
|
+
%w{
|
33
|
+
content_hash:string
|
34
|
+
content_type:string
|
35
|
+
content_length:integer
|
36
|
+
filename:string
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dis
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.9.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Inge Jørgensen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-23 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 4.1.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 4.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fog
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.22.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.22.1
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activejob
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: sqlite3
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 3.0.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 3.0.0
|
83
|
+
description: Dis is a Rails plugin that stores your file uploads and other binary
|
84
|
+
blobs.
|
85
|
+
email:
|
86
|
+
- inge@elektronaut.no
|
87
|
+
executables: []
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- MIT-LICENSE
|
92
|
+
- README.md
|
93
|
+
- lib/dis.rb
|
94
|
+
- lib/dis/errors.rb
|
95
|
+
- lib/dis/jobs.rb
|
96
|
+
- lib/dis/jobs/delete.rb
|
97
|
+
- lib/dis/jobs/store.rb
|
98
|
+
- lib/dis/layer.rb
|
99
|
+
- lib/dis/layers.rb
|
100
|
+
- lib/dis/model.rb
|
101
|
+
- lib/dis/model/class_methods.rb
|
102
|
+
- lib/dis/model/data.rb
|
103
|
+
- lib/dis/storage.rb
|
104
|
+
- lib/dis/validations.rb
|
105
|
+
- lib/dis/validations/data_presence.rb
|
106
|
+
- lib/dis/version.rb
|
107
|
+
- lib/rails/generators/dis/install/install_generator.rb
|
108
|
+
- lib/rails/generators/dis/install/templates/initializer.rb
|
109
|
+
- lib/rails/generators/dis/model/model_generator.rb
|
110
|
+
homepage: https://github.com/elektronaut/dis
|
111
|
+
licenses:
|
112
|
+
- MIT
|
113
|
+
metadata: {}
|
114
|
+
post_install_message:
|
115
|
+
rdoc_options: []
|
116
|
+
require_paths:
|
117
|
+
- lib
|
118
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
version: 1.9.2
|
123
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
requirements: []
|
129
|
+
rubyforge_project:
|
130
|
+
rubygems_version: 2.4.1
|
131
|
+
signing_key:
|
132
|
+
specification_version: 4
|
133
|
+
summary: A file store for your Rails app
|
134
|
+
test_files: []
|