Class: Aqua::Store::CouchDB::Attachments

Inherits:
Mash
  • Object
show all
Defined in:
lib/aqua/store/couch_db/attachments.rb

Overview

Attachments is a Hash-like container with keys that are attacment names and values that are file-type objects. Initializing and adding to the collection assures the types of both keys and values. The collection implements a lazy-loading scheme, such that when an attachment is requested and not found, it will try to load it from CouchDB.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(doc, hash = {}) ⇒ Attachments

Creates a new attachment collection with keys that are attachment names and values that are file-type objects. The collection manages both the key and the value types.

Parameters:

  • Document (String)

    uri; used to save and retrieve attachments directly

  • Initialization (Hash)

    values

Raises:

  • (ArgumentError)


19
20
21
22
23
24
# File 'lib/aqua/store/couch_db/attachments.rb', line 19

def initialize( doc, hash={} )
  raise ArgumentError, "must be initialized with a document" unless doc.respond_to?( :retrieve ) 
  @document = doc
  self.class.validate_hash( hash ) unless hash.empty?
  super( hash )
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



9
10
11
# File 'lib/aqua/store/couch_db/attachments.rb', line 9

def document
  @document
end

#stubsObject (readonly)

Returns the value of attribute stubs.



10
11
12
# File 'lib/aqua/store/couch_db/attachments.rb', line 10

def stubs
  @stubs
end

Class Method Details

.validate_hash(hash) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Validates and throws an error on a hash, insisting that the key is a string or symbol, and the value is a file.

Parameters:



131
132
133
134
135
136
# File 'lib/aqua/store/couch_db/attachments.rb', line 131

def self.validate_hash( hash )
  hash.each do |name, file|
    raise ArgumentError, "Attachment name, #{name.inspect}, must be a Symbol or a String" unless [Symbol, String ].include?( name.class )
    raise ArgumentError, "Attachment file, #{file.inspect}, must be a File-like object"  unless file.respond_to?( :read ) 
  end  
end

Instance Method Details

#add(name, file) ⇒ Object

Adds an attachment to the collection, checking for type. Does not add directly to the database.

Parameters:

  • Name (String, Symbol)

    of the attachment as a string or symbol

  • The (File)

    attachment



32
33
34
35
# File 'lib/aqua/store/couch_db/attachments.rb', line 32

def add( name, file )
  self.class.validate_hash( name => file )
  self[name] = file
end

#add!(name, file) ⇒ Object

Adds an attachment to the collection and to the database. Document doesn’t have to be saved, but it does need to have an id.

Parameters:

  • Name (String, Symbol)

    of the attachment as a string or symbol

  • The (File)

    attachment



44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/aqua/store/couch_db/attachments.rb', line 44

def add!( name, file )
  add( name, file )
  content_type = MIME::Types.type_for(file.path).first 
  content_type = content_type.nil? ? "text\/plain" : content_type.simplified
  data = {
    'content_type' => content_type,
    'data'         => Base64.encode64( file.read ).gsub(/\s/,'') 
  }
  file.rewind 
  response = CouchDB.put( uri_for( name ), data )
  update_doc_rev( response )
  file
end

#delete!(name) ⇒ File?

Deletes an attachment from the collection, and from the database. Use #delete (from Hash) to just delete the attachment from the collection.

Parameters:

  • Name (String, Symbol)

    of the attachment as a string or symbol

Returns:

  • (File, nil)

    File at that location or nil if no file found



65
66
67
68
69
70
71
72
73
# File 'lib/aqua/store/couch_db/attachments.rb', line 65

def delete!( name )
  if self[name]
    file = delete( name ) 
    unless document.new?
      CouchDB.delete( uri_for( name ) )
    end 
    file 
  end  
end

#get(name, stream = false) ⇒ File?

Gets an attachment from the collection first. If not found, it will be requested from the database.

Parameters:

Returns:

  • (File, nil)

    File for that name, or nil if not found in hash or in database



81
82
83
84
85
86
87
88
# File 'lib/aqua/store/couch_db/attachments.rb', line 81

def get( name, stream=false )
  file = self[name] 
  unless file
    file = get!( name, stream )
  end
  file.rewind if file # just in case of previous streaming
  file  
end

#get!(name, stream = false) ⇒ File?

TODO:

make this more memory favorable, maybe streaming/saving in a max number of bytes

Gets an attachment from the database. Stores it in the hash.

Parameters:

  • Name (String, Symbol)

    of the attachment

  • Stream (true, false)

    boolean flag indicating whether the data should be converted to a file or kept as a stream

Returns:

  • (File, nil)

    File for that name, or nil if not found in the database

Raises:

  • Any error encountered on retrieval of the attachment, json, http_client, Aqua etc



100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/aqua/store/couch_db/attachments.rb', line 100

def get!( name, stream=false ) 
  file = nil
  response = CouchDB.get( uri_for( name, false ), true ) rescue nil
  data = response && response.respond_to?(:keys) ? Base64.decode64( response['data'] ) : nil
  if data || response
    file = Tempfile.new( CGI.escape( name.to_s ) ) 
    file.binmode if file.respond_to?( :binmode )
    data ? file.write( data ) : file.write( response )
    file.rewind 
    self[name] = file
  end  
  stream ? file.read : file
end

#packObject

Creates a hash for the CouchDB _attachments key.

Examples:

"_attachments":
{
  "foo.txt":
  {
    "content_type":"text\/plain",
    "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
  },

 "bar.txt":
  {
    "content_type":"text\/plain",
    "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
  }
}


164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/aqua/store/couch_db/attachments.rb', line 164

def pack
  pack_hash = {} 
  self.keys.each do |key| 
    file = self[key]
    content_type = MIME::Types.type_for(file.path).first 
    content_type = content_type.nil? ? "text\/plain" : content_type.simplified
    data = {
      'content_type' => content_type,
      'data'         => Base64.encode64( file.read ).gsub(/\s/,'') 
    }
    file.rewind 
    pack_hash[key.to_s] = data
  end
  pack_hash  
end

#update_doc_rev(response) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Goes into the document and updates it’s rev to match the returned rev. That way #new? will return false when an attachment is created before the document is saved. It also means that future attempts to save the doc won’t fail with a conflict.

Parameters:

  • response (Hash)

    from the put request



144
145
146
# File 'lib/aqua/store/couch_db/attachments.rb', line 144

def update_doc_rev( response )
  document[:_rev] = response['rev']
end

#uri_for(name, include_rev = true) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Constructs the standalone attachment uri for PUT and DELETE actions.

Parameters:

  • Name (String)

    of the attachment as a string or symbol

Raises:

  • (ArgumentError)


119
120
121
122
# File 'lib/aqua/store/couch_db/attachments.rb', line 119

def uri_for( name, include_rev = true )
  raise ArgumentError, 'Document must have id in order to save an attachment' if document.id.nil? || document.id.empty?
  document.uri + "/#{CGI.escape( name.to_s )}" + ( document.rev && include_rev ? "?rev=#{document.rev}" : "" )
end