sufia-models 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|