sufia-models 0.0.1
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.md +177 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/app/models/batch.rb +46 -0
- data/app/models/checksum_audit_log.rb +35 -0
- data/app/models/contact_form.rb +42 -0
- data/app/models/datastreams/batch_rdf_datastream.rb +23 -0
- data/app/models/datastreams/file_content_datastream.rb +18 -0
- data/app/models/datastreams/fits_datastream.rb +188 -0
- data/app/models/datastreams/generic_file_rdf_datastream.rb +75 -0
- data/app/models/datastreams/paranoid_rights_datastream.rb +37 -0
- data/app/models/datastreams/properties_datastream.rb +33 -0
- data/app/models/domain_term.rb +18 -0
- data/app/models/follow.rb +28 -0
- data/app/models/generic_file.rb +16 -0
- data/app/models/geo_names_resource.rb +34 -0
- data/app/models/group.rb +8 -0
- data/app/models/local_authority.rb +93 -0
- data/app/models/local_authority_entry.rb +18 -0
- data/app/models/single_use_link.rb +26 -0
- data/app/models/subject_local_authority_entry.rb +16 -0
- data/app/models/trophy.rb +12 -0
- data/app/models/version_committer.rb +17 -0
- data/lib/sufia/models.rb +11 -0
- data/lib/sufia/models/active_fedora/redis.rb +49 -0
- data/lib/sufia/models/active_record/redis.rb +56 -0
- data/lib/sufia/models/engine.rb +34 -0
- data/lib/sufia/models/file_content.rb +9 -0
- data/lib/sufia/models/file_content/extract_metadata.rb +60 -0
- data/lib/sufia/models/file_content/versions.rb +23 -0
- data/lib/sufia/models/generic_file.rb +183 -0
- data/lib/sufia/models/generic_file/actions.rb +39 -0
- data/lib/sufia/models/generic_file/audit.rb +119 -0
- data/lib/sufia/models/generic_file/characterization.rb +81 -0
- data/lib/sufia/models/generic_file/export.rb +339 -0
- data/lib/sufia/models/generic_file/permissions.rb +64 -0
- data/lib/sufia/models/generic_file/thumbnail.rb +91 -0
- data/lib/sufia/models/id_service.rb +57 -0
- data/lib/sufia/models/jobs/audit_job.rb +65 -0
- data/lib/sufia/models/jobs/batch_update_job.rb +86 -0
- data/lib/sufia/models/jobs/characterize_job.rb +43 -0
- data/lib/sufia/models/jobs/content_delete_event_job.rb +31 -0
- data/lib/sufia/models/jobs/content_deposit_event_job.rb +32 -0
- data/lib/sufia/models/jobs/content_new_version_event_job.rb +32 -0
- data/lib/sufia/models/jobs/content_restored_version_event_job.rb +40 -0
- data/lib/sufia/models/jobs/content_update_event_job.rb +32 -0
- data/lib/sufia/models/jobs/event_job.rb +33 -0
- data/lib/sufia/models/jobs/ffmpeg_transcode_job.rb +61 -0
- data/lib/sufia/models/jobs/resolrize_job.rb +23 -0
- data/lib/sufia/models/jobs/transcode_audio_job.rb +40 -0
- data/lib/sufia/models/jobs/transcode_video_job.rb +39 -0
- data/lib/sufia/models/jobs/unzip_job.rb +54 -0
- data/lib/sufia/models/jobs/user_edit_profile_event_job.rb +35 -0
- data/lib/sufia/models/jobs/user_follow_event_job.rb +37 -0
- data/lib/sufia/models/jobs/user_unfollow_event_job.rb +38 -0
- data/lib/sufia/models/model_methods.rb +39 -0
- data/lib/sufia/models/noid.rb +42 -0
- data/lib/sufia/models/solr_document_behavior.rb +125 -0
- data/lib/sufia/models/user.rb +126 -0
- data/lib/sufia/models/utils.rb +36 -0
- data/lib/sufia/models/version.rb +5 -0
- data/lib/tasks/sufia-models_tasks.rake +4 -0
- data/sufia-models.gemspec +28 -0
- metadata +151 -0
data/lib/sufia/models.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveFedora
|
2
|
+
class UnsavedDigitalObject
|
3
|
+
def assign_pid
|
4
|
+
@pid ||= Sufia::IdService.mint
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class Base
|
9
|
+
def stream
|
10
|
+
Nest.new(self.class.name, $redis)[to_param]
|
11
|
+
rescue
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.stream
|
16
|
+
Nest.new(name, $redis)
|
17
|
+
rescue
|
18
|
+
nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def events(size=-1)
|
22
|
+
stream[:event].lrange(0, size).map do |event_id|
|
23
|
+
{
|
24
|
+
action: $redis.hget("events:#{event_id}", "action"),
|
25
|
+
timestamp: $redis.hget("events:#{event_id}", "timestamp")
|
26
|
+
}
|
27
|
+
end
|
28
|
+
rescue
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_event(action, timestamp)
|
33
|
+
event_id = $redis.incr("events:latest_id")
|
34
|
+
$redis.hmset("events:#{event_id}", "action", action, "timestamp", timestamp)
|
35
|
+
event_id
|
36
|
+
rescue
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_event(event_id)
|
41
|
+
stream[:event].lpush(event_id)
|
42
|
+
rescue
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
@@ -0,0 +1,56 @@
|
|
1
|
+
ActiveRecord::Base.class_eval do
|
2
|
+
def stream
|
3
|
+
Nest.new(self.class.name, $redis)[to_param]
|
4
|
+
rescue
|
5
|
+
nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.stream
|
9
|
+
Nest.new(name, $redis)
|
10
|
+
rescue
|
11
|
+
nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def events(size=-1)
|
15
|
+
stream[:event].lrange(0, size).map do |event_id|
|
16
|
+
{
|
17
|
+
action: $redis.hget("events:#{event_id}", "action"),
|
18
|
+
timestamp: $redis.hget("events:#{event_id}", "timestamp")
|
19
|
+
}
|
20
|
+
end
|
21
|
+
rescue
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
|
25
|
+
def profile_events(size=-1)
|
26
|
+
stream[:event][:profile].lrange(0, size).map do |event_id|
|
27
|
+
{
|
28
|
+
action: $redis.hget("events:#{event_id}", "action"),
|
29
|
+
timestamp: $redis.hget("events:#{event_id}", "timestamp")
|
30
|
+
}
|
31
|
+
end
|
32
|
+
rescue
|
33
|
+
[]
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_event(action, timestamp)
|
37
|
+
event_id = $redis.incr("events:latest_id")
|
38
|
+
$redis.hmset("events:#{event_id}", "action", action, "timestamp", timestamp)
|
39
|
+
event_id
|
40
|
+
rescue
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def log_event(event_id)
|
45
|
+
stream[:event].lpush(event_id)
|
46
|
+
rescue
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def log_profile_event(event_id)
|
51
|
+
stream[:event][:profile].lpush(event_id)
|
52
|
+
rescue
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Curate
|
2
|
+
module Models
|
3
|
+
class Engine < ::Rails::Engine
|
4
|
+
config.autoload_paths += %W(
|
5
|
+
#{config.root}/lib/sufia/models/jobs
|
6
|
+
)
|
7
|
+
initializer "Patch active_fedora" do
|
8
|
+
require 'sufia/models/active_fedora/redis'
|
9
|
+
end
|
10
|
+
|
11
|
+
initializer "Patch active_record" do
|
12
|
+
require 'sufia/models/active_record/redis'
|
13
|
+
end
|
14
|
+
|
15
|
+
initializer 'requires' do
|
16
|
+
require 'sufia/models/model_methods'
|
17
|
+
require 'sufia/models/noid'
|
18
|
+
require 'sufia/models/file_content'
|
19
|
+
require 'sufia/models/file_content/extract_metadata'
|
20
|
+
require 'sufia/models/file_content/versions'
|
21
|
+
require 'sufia/models/generic_file/actions'
|
22
|
+
require 'sufia/models/generic_file/audit'
|
23
|
+
require 'sufia/models/generic_file/characterization'
|
24
|
+
require 'sufia/models/generic_file/export'
|
25
|
+
require 'sufia/models/generic_file/permissions'
|
26
|
+
require 'sufia/models/generic_file/thumbnail'
|
27
|
+
require 'sufia/models/generic_file'
|
28
|
+
require 'sufia/models/user'
|
29
|
+
require 'sufia/models/id_service'
|
30
|
+
require 'sufia/models/solr_document_behavior'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'open3'
|
2
|
+
module Sufia
|
3
|
+
module FileContent
|
4
|
+
module ExtractMetadata
|
5
|
+
include Open3
|
6
|
+
|
7
|
+
def extract_metadata
|
8
|
+
out = nil
|
9
|
+
to_tempfile do |f|
|
10
|
+
out = run_fits!(f.path)
|
11
|
+
end
|
12
|
+
out
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_tempfile &block
|
16
|
+
return unless has_content?
|
17
|
+
tmp_base = Sufia::Engine.config.temp_file_base
|
18
|
+
f = Tempfile.new("#{pid}-#{dsVersionID}")
|
19
|
+
f.binmode
|
20
|
+
if content.respond_to? :read
|
21
|
+
f.write(content.read)
|
22
|
+
else
|
23
|
+
f.write(content)
|
24
|
+
end
|
25
|
+
f.close
|
26
|
+
content.rewind if content.respond_to? :rewind
|
27
|
+
yield(f)
|
28
|
+
f.unlink
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return true if the content is present
|
32
|
+
# You can override this method if your content is an external datastream
|
33
|
+
def has_content?
|
34
|
+
!content.nil?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
|
40
|
+
def run_fits!(file_path)
|
41
|
+
command = "#{fits_path} -i \"#{file_path}\""
|
42
|
+
stdin, stdout, stderr, wait_thr = popen3(command)
|
43
|
+
stdin.close
|
44
|
+
out = stdout.read
|
45
|
+
stdout.close
|
46
|
+
err = stderr.read
|
47
|
+
stderr.close
|
48
|
+
exit_status = wait_thr.value
|
49
|
+
raise "Unable to execute command \"#{command}\"\n#{err}" unless exit_status.success?
|
50
|
+
out
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
def fits_path
|
55
|
+
Sufia::Engine.config.fits_path
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Sufia
|
2
|
+
module FileContent
|
3
|
+
module Versions
|
4
|
+
def get_version(version_id)
|
5
|
+
self.versions.select { |v| v.versionID == version_id}.first
|
6
|
+
end
|
7
|
+
|
8
|
+
def latest_version
|
9
|
+
self.versions.first
|
10
|
+
end
|
11
|
+
|
12
|
+
def version_committer(version)
|
13
|
+
vc = VersionCommitter.where(:obj_id => version.pid,
|
14
|
+
:datastream_id => version.dsid,
|
15
|
+
:version_id => version.versionID)
|
16
|
+
return vc.empty? ? nil : vc.first.committer_login
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require 'datastreams/generic_file_rdf_datastream'
|
2
|
+
require 'datastreams/properties_datastream'
|
3
|
+
require 'datastreams/file_content_datastream'
|
4
|
+
|
5
|
+
module Sufia
|
6
|
+
module GenericFile
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
extend ActiveSupport::Autoload
|
9
|
+
include Sufia::ModelMethods
|
10
|
+
include Sufia::Noid
|
11
|
+
include Sufia::GenericFile::Thumbnail
|
12
|
+
include Sufia::GenericFile::Export
|
13
|
+
include Sufia::GenericFile::Characterization
|
14
|
+
include Sufia::GenericFile::Audit
|
15
|
+
include Sufia::GenericFile::Permissions
|
16
|
+
|
17
|
+
included do
|
18
|
+
has_metadata :name => "descMetadata", :type => GenericFileRdfDatastream
|
19
|
+
has_metadata :name => "properties", :type => PropertiesDatastream
|
20
|
+
has_file_datastream :name => "content", :type => FileContentDatastream
|
21
|
+
has_file_datastream :name => "thumbnail"
|
22
|
+
|
23
|
+
belongs_to :batch, :property => :is_part_of
|
24
|
+
|
25
|
+
delegate_to :properties, [:relative_path, :depositor], :unique => true
|
26
|
+
delegate_to :descMetadata, [:date_uploaded, :date_modified], :unique => true
|
27
|
+
delegate_to :descMetadata, [:related_url, :based_near, :part_of, :creator,
|
28
|
+
:contributor, :title, :tag, :description, :rights,
|
29
|
+
:publisher, :date_created, :subject,
|
30
|
+
:resource_type, :identifier, :language]
|
31
|
+
|
32
|
+
around_save :characterize_if_changed, :retry_warming
|
33
|
+
before_save :remove_blank_assertions
|
34
|
+
before_destroy :cleanup_trophies
|
35
|
+
end
|
36
|
+
|
37
|
+
def remove_blank_assertions
|
38
|
+
terms_for_editing.each do |key|
|
39
|
+
self[key] = nil if self[key] == ['']
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def record_version_committer(user)
|
45
|
+
version = content.latest_version
|
46
|
+
# content datastream not (yet?) present
|
47
|
+
return if version.nil?
|
48
|
+
VersionCommitter.create(:obj_id => version.pid,
|
49
|
+
:datastream_id => version.dsid,
|
50
|
+
:version_id => version.versionID,
|
51
|
+
:committer_login => user.user_key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def pdf?
|
55
|
+
['application/pdf'].include? self.mime_type
|
56
|
+
end
|
57
|
+
|
58
|
+
def image?
|
59
|
+
['image/png','image/jpeg', 'image/jpg', 'image/jp2', 'image/bmp', 'image/gif'].include? self.mime_type
|
60
|
+
end
|
61
|
+
|
62
|
+
def video?
|
63
|
+
['video/mpeg', 'video/mp4', 'video/webm', 'video/x-msvideo', 'video/avi', 'video/quicktime', 'application/mxf'].include? self.mime_type
|
64
|
+
end
|
65
|
+
|
66
|
+
def audio?
|
67
|
+
# audio/x-wave is the mime type that fits 0.6.0 returns for a wav file.
|
68
|
+
# audio/mpeg is the mime type that fits 0.6.0 returns for an mp3 file.
|
69
|
+
['audio/mp3', 'audio/mpeg', 'audio/x-wave', 'audio/x-wav', 'audio/ogg'].include? self.mime_type
|
70
|
+
end
|
71
|
+
|
72
|
+
def persistent_url
|
73
|
+
"#{Sufia::Engine.config.persistent_hostpath}#{noid}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def retry_warming
|
77
|
+
save_tries = 0
|
78
|
+
conflict_tries = 0
|
79
|
+
begin
|
80
|
+
yield
|
81
|
+
rescue RSolr::Error::Http => error
|
82
|
+
save_tries += 1
|
83
|
+
logger.warn "Retry Solr caught RSOLR error on #{self.pid}: #{error.inspect}"
|
84
|
+
# fail for good if the tries is greater than 3
|
85
|
+
raise if save_tries >=3
|
86
|
+
sleep 0.01
|
87
|
+
retry
|
88
|
+
rescue ActiveResource::ResourceConflict => error
|
89
|
+
conflict_tries += 1
|
90
|
+
logger.warn "Retry caught Active Resource Conflict #{self.pid}: #{error.inspect}"
|
91
|
+
raise if conflict_tries >=10
|
92
|
+
sleep 0.01
|
93
|
+
retry
|
94
|
+
rescue =>error
|
95
|
+
if (error.to_s.downcase.include? "conflict")
|
96
|
+
conflict_tries += 1
|
97
|
+
logger.warn "Retry caught Active Resource Conflict #{self.pid}: #{error.inspect}"
|
98
|
+
raise if conflict_tries >=10
|
99
|
+
sleep 0.01
|
100
|
+
retry
|
101
|
+
else
|
102
|
+
raise
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def cleanup_trophies
|
108
|
+
Trophy.destroy_all(generic_file_id: self.noid)
|
109
|
+
end
|
110
|
+
|
111
|
+
def related_files
|
112
|
+
relateds = begin
|
113
|
+
self.batch.generic_files
|
114
|
+
rescue NoMethodError => e
|
115
|
+
#batch is nil - When would this ever happen?
|
116
|
+
batch_id = self.object_relations["isPartOf"].first || self.object_relations[:is_part_of].first
|
117
|
+
return [] if batch_id.nil?
|
118
|
+
self.class.find(Solrizer.solr_name('is_part_of', :symbol) => batch_id)
|
119
|
+
end
|
120
|
+
relateds.reject { |gf| gf.pid == self.pid }
|
121
|
+
end
|
122
|
+
|
123
|
+
# Unstemmed, searchable, stored
|
124
|
+
def self.noid_indexer
|
125
|
+
@noid_indexer ||= Solrizer::Descriptor.new(:text, :indexed, :stored)
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_solr(solr_doc={}, opts={})
|
129
|
+
super(solr_doc, opts)
|
130
|
+
solr_doc[Solrizer.solr_name('label')] = self.label
|
131
|
+
solr_doc[Solrizer.solr_name('noid', Sufia::GenericFile.noid_indexer)] = noid
|
132
|
+
solr_doc[Solrizer.solr_name('file_format')] = file_format
|
133
|
+
solr_doc[Solrizer.solr_name('file_format', :facetable)] = file_format
|
134
|
+
return solr_doc
|
135
|
+
end
|
136
|
+
|
137
|
+
def file_format
|
138
|
+
return nil if self.mime_type.blank? and self.format_label.blank?
|
139
|
+
return self.mime_type.split('/')[1]+ " ("+self.format_label.join(", ")+")" unless self.mime_type.blank? or self.format_label.blank?
|
140
|
+
return self.mime_type.split('/')[1] unless self.mime_type.blank?
|
141
|
+
return self.format_label
|
142
|
+
end
|
143
|
+
|
144
|
+
# Redefine this for more intuitive keys in Redis
|
145
|
+
def to_param
|
146
|
+
noid
|
147
|
+
end
|
148
|
+
|
149
|
+
def label=(new_label)
|
150
|
+
@inner_object.label = new_label
|
151
|
+
if self.title.empty?
|
152
|
+
self.title = new_label
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_jq_upload
|
157
|
+
return {
|
158
|
+
"name" => self.title,
|
159
|
+
"size" => self.file_size,
|
160
|
+
"url" => "/files/#{noid}",
|
161
|
+
"thumbnail_url" => self.pid,
|
162
|
+
"delete_url" => "deleteme", # generic_file_path(:id => id),
|
163
|
+
"delete_type" => "DELETE"
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
def terms_for_editing
|
168
|
+
terms_for_display -
|
169
|
+
[:part_of, :date_modified, :date_uploaded, :format] #, :resource_type]
|
170
|
+
end
|
171
|
+
|
172
|
+
def terms_for_display
|
173
|
+
self.descMetadata.class.config.keys
|
174
|
+
end
|
175
|
+
|
176
|
+
# Is this file in the middle of being processed by a batch?
|
177
|
+
def processing?
|
178
|
+
return false if self.batch.blank?
|
179
|
+
return false if !self.batch.methods.include? :status
|
180
|
+
return (!self.batch.status.empty?) && (self.batch.status.count == 1) && (self.batch.status[0] == "processing")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Sufia::GenericFile
|
2
|
+
# Actions are decoupled from controller logic so that they may be called from a controller or a background job.
|
3
|
+
module Actions
|
4
|
+
def self.create_metadata(generic_file, user, batch_id)
|
5
|
+
|
6
|
+
generic_file.apply_depositor_metadata(user.user_key)
|
7
|
+
generic_file.date_uploaded = Date.today
|
8
|
+
generic_file.date_modified = Date.today
|
9
|
+
generic_file.creator = user.name
|
10
|
+
|
11
|
+
if batch_id
|
12
|
+
generic_file.add_relationship("isPartOf", "info:fedora/#{Sufia::Noid.namespaceize(batch_id)}")
|
13
|
+
else
|
14
|
+
logger.warn "unable to find batch to attach to"
|
15
|
+
end
|
16
|
+
generic_file.save!
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.create_content(generic_file, file, file_name, dsid, user)
|
20
|
+
generic_file.add_file(file, dsid, file_name)
|
21
|
+
|
22
|
+
save_tries = 0
|
23
|
+
begin
|
24
|
+
generic_file.save!
|
25
|
+
rescue RSolr::Error::Http => error
|
26
|
+
logger.warn "GenericFilesController::create_and_save_generic_file Caught RSOLR error #{error.inspect}"
|
27
|
+
save_tries+=1
|
28
|
+
# fail for good if the tries is greater than 3
|
29
|
+
raise error if save_tries >=3
|
30
|
+
sleep 0.01
|
31
|
+
retry
|
32
|
+
end
|
33
|
+
|
34
|
+
generic_file.record_version_committer(user)
|
35
|
+
Sufia.queue.push(UnzipJob.new(generic_file.pid)) if generic_file.content.mimeType == 'application/zip'
|
36
|
+
Sufia.queue.push(ContentDepositEventJob.new(generic_file.pid, user.user_key))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|